Merge branch 'develop' into fix-spelling-a-char

This commit is contained in:
Ahmed Bouhuolia
2023-08-22 22:49:39 +02:00
301 changed files with 9345 additions and 7022 deletions

71
.all-contributorsrc Normal file
View File

@@ -0,0 +1,71 @@
{
"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"
]
},
{
"login": "KalliopiPliogka",
"name": "Kalliopi Pliogka",
"avatar_url": "https://avatars.githubusercontent.com/u/81677549?v=4",
"profile": "https://github.com/KalliopiPliogka",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "bigcapital",
"projectOwner": "bigcapitalhq"
}

View File

@@ -47,3 +47,6 @@ AGENDASH_AUTH_PASSWORD=123123
SIGNUP_DISABLED=false SIGNUP_DISABLED=false
SIGNUP_ALLOWED_DOMAINS= SIGNUP_ALLOWED_DOMAINS=
SIGNUP_ALLOWED_EMAILS= SIGNUP_ALLOWED_EMAILS=
# API rate limit (points,duration,block duration).
API_RATE_LIMIT=120,60,600

View File

@@ -2,6 +2,29 @@
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 # [0.9.8] - 19-06-2023
`bigcapitalhq/webapp` `bigcapitalhq/webapp`

View File

@@ -7,6 +7,7 @@ Please read through this document before submitting any issues or pull requests
## Sections ## Sections
- [General Instructions](#general-instructions) - [General Instructions](#general-instructions)
- [Local Setup Prerequisites](#local-setup-prerequisites)
- [Contribute to Backend](#contribute-to-backend) - [Contribute to Backend](#contribute-to-backend)
- [Contribute to Frontend](#contribute-to-frontend) - [Contribute to Frontend](#contribute-to-frontend)
- [Other Ways to Contribute](#other-ways-to-contribute) - [Other Ways to Contribute](#other-ways-to-contribute)
@@ -31,9 +32,18 @@ Contributions via pull requests are much appreciated. Once the approach is agree
--- ---
## Local Setup Prerequisites
- The application currently supports **Node.js v14.x**. Please ensure that you are using this version of Node.js when developing. (use [nvm](https://github.com/nvm-sh/nvm#installing-and-updating) to switch between node versions)
## Contribute to Backend ## Contribute to Backend
- Clone the `bigcapital` repository and `cd` into `bigcapital` directory. - Clone the `bigcapital` repository and `cd` into `bigcapital` directory.
- Create `.env` file by copying `.env.example` file to `.env`. (The ``.env.example`` file has all the necessary values of variables to start development directly).
```
cp .env.example .env
```
- 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 these command on root directory and it will install dependencies of all packages.
``` ```

View File

@@ -47,3 +47,40 @@ Bigcapital is a smart and open-source accounting and inventory software, Bigcapi
# Changelog # Changelog
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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</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

@@ -21,10 +21,16 @@ services:
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
@@ -37,6 +43,9 @@ 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}
@@ -93,6 +102,9 @@ services:
mysql: mysql:
container_name: bigcapital-mysql container_name: bigcapital-mysql
deploy:
restart_policy:
condition: unless-stopped
build: build:
context: ./docker/mariadb context: ./docker/mariadb
environment: environment:
@@ -106,7 +118,10 @@ services:
- '3306' - '3306'
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'
@@ -115,6 +130,9 @@ 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

@@ -20,6 +20,9 @@ services:
- '3306' - '3306'
ports: ports:
- '3306:3306' - '3306:3306'
deploy:
restart_policy:
condition: unless-stopped
mongo: mongo:
build: ./docker/mongo build: ./docker/mongo
@@ -29,6 +32,9 @@ 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:
@@ -37,6 +43,9 @@ services:
- "6379" - "6379"
volumes: volumes:
- redis:/data - redis:/data
deploy:
restart_policy:
condition: unless-stopped
# Volumes # Volumes
volumes: volumes:

13
e2e/_utils.ts Normal file
View File

@@ -0,0 +1,13 @@
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,14 +1,23 @@
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(); authPage = await browser.newPage({ ...defaultPageConfig() });
});
test.afterAll(async () => {
await authPage.close();
});
test.afterEach(async ({ context }) => {
context.clearCookies();
await clearLocalStorage(authPage);
}); });
test.describe('login', () => { test.describe('login', () => {
test.beforeAll(async () => { test.beforeEach(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 () => {
@@ -30,10 +39,23 @@ 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.beforeAll(async () => { test.beforeEach(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 () => {
@@ -52,10 +74,36 @@ 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 () => { test.beforeAll(async ({ browser }) => {
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 () => {

7
e2e/items.spec.ts Normal file
View File

@@ -0,0 +1,7 @@
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.', () => {});
});

86
e2e/onboarding.spec.ts Normal file
View File

@@ -0,0 +1,86 @@
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);
});
});
});

14
package-lock.json generated
View File

@@ -333,6 +333,12 @@
"@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",
@@ -945,6 +951,7 @@
"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",
@@ -954,7 +961,8 @@
"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
} }
} }
}, },
@@ -1003,7 +1011,8 @@
"@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",
@@ -2324,6 +2333,7 @@
"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,12 +18,13 @@
"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

@@ -1,26 +1,27 @@
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 { Service, Inject } from 'typedi'; import {
import { AbilitySubject, BillAction, IBillDTO, IBillEditDTO } from '@/interfaces'; AbilitySubject,
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 BillPaymentsService from '@/services/Purchases/BillPaymentsService'; import { BillsApplication } from '@/services/Purchases/Bills/BillsApplication';
@Service() @Service()
export default class BillsController extends BaseController { export default class BillsController extends BaseController {
@Inject() @Inject()
private billsService: BillsService; private billsApplication: BillsApplication;
@Inject() @Inject()
private dynamicListService: DynamicListingService; private dynamicListService: DynamicListingService;
@Inject()
private billPayments: BillPaymentsService;
/** /**
* Router constructor. * Router constructor.
*/ */
@@ -97,7 +98,7 @@ export default class BillsController extends BaseController {
/** /**
* Common validation schema. * Common validation schema.
*/ */
get billValidationSchema() { private 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(),
@@ -142,7 +143,7 @@ export default class BillsController extends BaseController {
/** /**
* Common validation schema. * Common validation schema.
*/ */
get billEditValidationSchema() { private 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(),
@@ -184,14 +185,14 @@ export default class BillsController extends BaseController {
/** /**
* Bill validation schema. * Bill validation schema.
*/ */
get specificBillValidationSchema() { private get specificBillValidationSchema() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* Bills list validation schema. * Bills list validation schema.
*/ */
get billsListingValidationSchema() { private 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(),
@@ -203,7 +204,7 @@ export default class BillsController extends BaseController {
]; ];
} }
get dueBillsListingValidationSchema() { private 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(),
@@ -216,17 +217,16 @@ export default class BillsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async newBill(req: Request, res: Response, next: NextFunction) { private 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.billsService.createBill( const storedBill = await this.billsApplication.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
*/ */
async editBill(req: Request, res: Response, next: NextFunction) { private 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.billsService.editBill(tenantId, billId, billDTO, user); await this.billsApplication.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 -
*/ */
async openBill(req: Request, res: Response, next: NextFunction) { private 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.billsService.openBill(tenantId, billId); await this.billsApplication.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}
*/ */
async getBill(req: Request, res: Response, next: NextFunction) { private 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.billsService.getBill(tenantId, billId); const bill = await this.billsApplication.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}
*/ */
async deleteBill(req: Request, res: Response, next: NextFunction) { private 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.billsService.deleteBill(tenantId, billId); await this.billsApplication.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}
*/ */
public async billsList(req: Request, res: Response, next: NextFunction) { private 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.billsService.getBills(tenantId, filter); await this.billsApplication.getBills(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
bills: this.transfromToResponse(bills), bills: this.transfromToResponse(bills),
@@ -356,12 +356,13 @@ export default class BillsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
public async getDueBills(req: Request, res: Response, next: NextFunction) { private 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.billsService.getDueBills(tenantId, vendorId); const bills = await this.billsApplication.getDueBills(tenantId, vendorId);
return res.status(200).send({ bills }); return res.status(200).send({ bills });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -374,7 +375,7 @@ export default class BillsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
public getBillPaymentsTransactions = async ( private getBillPaymentsTransactions = async (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -383,7 +384,7 @@ export default class BillsController extends BaseController {
const { id: billId } = req.params; const { id: billId } = req.params;
try { try {
const billPayments = await this.billPayments.getBillPayments( const billPayments = await this.billsApplication.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 BillPaymentsService from '@/services/Purchases/BillPayments/BillPayments'; import { BillPaymentsApplication } from '@/services/Purchases/BillPayments/BillPaymentsApplication';
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()
billPaymentService: BillPaymentsService; private billPaymentsApplication: BillPaymentsApplication;
@Inject() @Inject()
dynamicListService: DynamicListingService; private dynamicListService: DynamicListingService;
@Inject() @Inject()
billPaymentsPages: BillPaymentsPages; private billPaymentsPages: BillPaymentsPages;
/** /**
* Router constructor. * Router constructor.
*/ */
router() { public 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[]}
*/ */
get billPaymentSchemaValidation(): ValidationChain[] { private 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[]}
*/ */
get specificBillPaymentValidateSchema(): ValidationChain[] { private 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[]}
*/ */
get listingValidationSchema(): ValidationChain[] { private 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 -
*/ */
async getBillPaymentNewPageEntries(req: Request, res: Response) { private 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
*/ */
async getBillPaymentEditPage( private async getBillPaymentEditPage(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -205,16 +205,19 @@ export default class BillsPayments extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Response} res * @param {Response} res
*/ */
async createBillPayment(req: Request, res: Response, next: NextFunction) { private async createBillPayment(
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.billPaymentService.createBillPayment( const billPayment = await this.billPaymentsApplication.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.',
@@ -229,13 +232,17 @@ export default class BillsPayments extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async editBillPayment(req: Request, res: Response, next: NextFunction) { private async editBillPayment(
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.billPaymentService.editBillPayment( const paymentMade = await this.billPaymentsApplication.editBillPayment(
tenantId, tenantId,
billPaymentId, billPaymentId,
billPaymentDTO billPaymentDTO
@@ -256,12 +263,19 @@ export default class BillsPayments extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} res - * @return {Response} res -
*/ */
async deleteBillPayment(req: Request, res: Response, next: NextFunction) { private async deleteBillPayment(
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.billPaymentService.deleteBillPayment(tenantId, billPaymentId); await this.billPaymentsApplication.deleteBillPayment(
tenantId,
billPaymentId
);
return res.status(200).send({ return res.status(200).send({
id: billPaymentId, id: billPaymentId,
@@ -277,16 +291,19 @@ export default class BillsPayments extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async getBillPayment(req: Request, res: Response, next: NextFunction) { private async getBillPayment(
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.billPaymentService.getBillPayment( const billPayment = await this.billPaymentsApplication.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),
}); });
@@ -301,12 +318,16 @@ export default class BillsPayments extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
async getPaymentBills(req: Request, res: Response, next: NextFunction) { private async getPaymentBills(
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.billPaymentService.getPaymentBills( const bills = await this.billPaymentsApplication.getPaymentBills(
tenantId, tenantId,
billPaymentId billPaymentId
); );
@@ -322,7 +343,11 @@ export default class BillsPayments extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} * @return {Response}
*/ */
async getBillsPayments(req: Request, res: Response, next: NextFunction) { private async getBillsPayments(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const billPaymentsFilter = { const billPaymentsFilter = {
page: 1, page: 1,
@@ -335,7 +360,7 @@ export default class BillsPayments extends BaseController {
try { try {
const { billPayments, pagination, filterMeta } = const { billPayments, pagination, filterMeta } =
await this.billPaymentService.listBillPayments( await this.billPaymentsApplication.getBillPayments(
tenantId, tenantId,
billPaymentsFilter billPaymentsFilter
); );
@@ -357,7 +382,7 @@ export default class BillsPayments extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
handleServiceError( private handleServiceError(
error: Error, error: Error,
req: Request, req: Request,
res: Response, res: Response,

View File

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

View File

@@ -1,20 +1,17 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator'; import { check, param, query } 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',
@@ -23,21 +20,15 @@ const ACCEPT_TYPE = {
@Service() @Service()
export default class SalesEstimatesController extends BaseController { export default class SalesEstimatesController extends BaseController {
@Inject() @Inject()
saleEstimateService: SaleEstimateService; private saleEstimatesApplication: SaleEstimatesApplication;
@Inject() @Inject()
dynamicListService: DynamicListingService; private dynamicListService: DynamicListingService;
@Inject()
saleEstimatesPdf: SaleEstimatesPdfService;
@Inject()
saleEstimateNotifySms: SaleEstimateNotifyBySms;
/** /**
* Router constructor. * Router constructor.
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -136,7 +127,7 @@ export default class SalesEstimatesController extends BaseController {
/** /**
* Estimate validation schema. * Estimate validation schema.
*/ */
get estimateValidationSchema() { private 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(),
@@ -177,14 +168,14 @@ export default class SalesEstimatesController extends BaseController {
/** /**
* Specific sale estimate validation schema. * Specific sale estimate validation schema.
*/ */
get validateSpecificEstimateSchema() { private get validateSpecificEstimateSchema() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* Sales estimates list validation schema. * Sales estimates list validation schema.
*/ */
get validateEstimateListSchema() { private 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(),
@@ -202,15 +193,16 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} res - * @return {Response} res -
*/ */
async newEstimate(req: Request, res: Response, next: NextFunction) { private 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 = await this.saleEstimateService.createEstimate( const storedEstimate =
tenantId, await this.saleEstimatesApplication.createSaleEstimate(
estimateDTO tenantId,
); estimateDTO
);
return res.status(200).send({ return res.status(200).send({
id: storedEstimate.id, id: storedEstimate.id,
@@ -226,19 +218,18 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async editEstimate(req: Request, res: Response, next: NextFunction) { private 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.saleEstimateService.editEstimate( await this.saleEstimatesApplication.editSaleEstimate(
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.',
@@ -253,13 +244,19 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async deleteEstimate(req: Request, res: Response, next: NextFunction) { private async deleteEstimate(
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.saleEstimateService.deleteEstimate(tenantId, estimateId); await this.saleEstimatesApplication.deleteSaleEstimate(
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.',
@@ -274,13 +271,19 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async deliverSaleEstimate(req: Request, res: Response, next: NextFunction) { private async deliverSaleEstimate(
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.saleEstimateService.deliverSaleEstimate(tenantId, estimateId); await this.saleEstimatesApplication.deliverSaleEstimate(
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.',
@@ -296,12 +299,19 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
async approveSaleEstimate(req: Request, res: Response, next: NextFunction) { private async approveSaleEstimate(
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.saleEstimateService.approveSaleEstimate(tenantId, estimateId); await this.saleEstimatesApplication.approveSaleEstimate(
tenantId,
estimateId
);
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
@@ -318,12 +328,19 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
async rejectSaleEstimate(req: Request, res: Response, next: NextFunction) { private async rejectSaleEstimate(
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.saleEstimateService.rejectSaleEstimate(tenantId, estimateId); await this.saleEstimatesApplication.rejectSaleEstimate(
tenantId,
estimateId
);
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
@@ -340,12 +357,12 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
async getEstimate(req: Request, res: Response, next: NextFunction) { private 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.saleEstimateService.getEstimate( const estimate = await this.saleEstimatesApplication.getSaleEstimate(
tenantId, tenantId,
estimateId estimateId
); );
@@ -357,10 +374,11 @@ export default class SalesEstimatesController extends BaseController {
}, },
// PDF content type. // PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => { [ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleEstimatesPdf.saleEstimatePdf( const pdfContent =
tenantId, await this.saleEstimatesApplication.getSaleEstimatePdf(
estimate tenantId,
); estimate
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
@@ -378,7 +396,7 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async getEstimates(req: Request, res: Response, next: NextFunction) { private async getEstimates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
sortOrder: 'desc', sortOrder: 'desc',
@@ -390,7 +408,7 @@ export default class SalesEstimatesController extends BaseController {
try { try {
const { salesEstimates, pagination, filterMeta } = const { salesEstimates, pagination, filterMeta } =
await this.saleEstimateService.estimatesList(tenantId, filter); await this.saleEstimatesApplication.getSaleEstimates(tenantId, filter);
res.format({ res.format({
[ACCEPT_TYPE.APPLICATION_JSON]: () => { [ACCEPT_TYPE.APPLICATION_JSON]: () => {
@@ -408,7 +426,7 @@ export default class SalesEstimatesController extends BaseController {
} }
} }
public saleEstimateNotifyBySms = async ( private saleEstimateNotifyBySms = async (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -417,10 +435,11 @@ export default class SalesEstimatesController extends BaseController {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
try { try {
const saleEstimate = await this.saleEstimateNotifySms.notifyBySms( const saleEstimate =
tenantId, await this.saleEstimatesApplication.notifySaleEstimateBySms(
estimateId tenantId,
); estimateId
);
return res.status(200).send({ return res.status(200).send({
id: saleEstimate.id, id: saleEstimate.id,
message: message:
@@ -437,7 +456,7 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
public saleEstimateSmsDetails = async ( private saleEstimateSmsDetails = async (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -446,10 +465,11 @@ export default class SalesEstimatesController extends BaseController {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
try { try {
const estimateSmsDetails = await this.saleEstimateNotifySms.smsDetails( const estimateSmsDetails =
tenantId, await this.saleEstimatesApplication.getSaleEstimateSmsDetails(
estimateId tenantId,
); estimateId
);
return res.status(200).send({ return res.status(200).send({
data: estimateSmsDetails, data: estimateSmsDetails,
}); });

View File

@@ -3,7 +3,6 @@ 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 {
@@ -12,11 +11,8 @@ 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 InvoicePaymentsService from '@/services/Sales/Invoices/InvoicePaymentsService'; import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
const ACCEPT_TYPE = { const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf', APPLICATION_PDF: 'application/pdf',
@@ -25,27 +21,15 @@ const ACCEPT_TYPE = {
@Service() @Service()
export default class SaleInvoicesController extends BaseController { export default class SaleInvoicesController extends BaseController {
@Inject() @Inject()
saleInvoiceService: SaleInvoiceService; private saleInvoiceApplication: SaleInvoiceApplication;
@Inject() @Inject()
dynamicListService: DynamicListingService; private dynamicListService: DynamicListingService;
@Inject()
saleInvoicePdf: SaleInvoicePdf;
@Inject()
saleInvoiceWriteoff: SaleInvoiceWriteoff;
@Inject()
saleInvoiceSmsNotify: SaleInvoiceNotifyBySms;
@Inject()
invoicePaymentsSerivce: InvoicePaymentsService;
/** /**
* Router constructor. * Router constructor.
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -167,7 +151,7 @@ export default class SaleInvoicesController extends BaseController {
/** /**
* Sale invoice validation schema. * Sale invoice validation schema.
*/ */
get saleInvoiceValidationSchema() { private 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(),
@@ -227,14 +211,14 @@ export default class SaleInvoicesController extends BaseController {
/** /**
* Specific sale invoice validation schema. * Specific sale invoice validation schema.
*/ */
get specificSaleInvoiceValidation() { private get specificSaleInvoiceValidation() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* Sales invoices list validation schema. * Sales invoices list validation schema.
*/ */
get saleInvoiceListValidationSchema() { private 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(),
@@ -249,7 +233,7 @@ export default class SaleInvoicesController extends BaseController {
/** /**
* Due sale invoice list validation schema. * Due sale invoice list validation schema.
*/ */
get dueSalesInvoicesListValidationSchema() { private get dueSalesInvoicesListValidationSchema() {
return [query('customer_id').optional().isNumeric().toInt()]; return [query('customer_id').optional().isNumeric().toInt()];
} }
@@ -259,17 +243,22 @@ export default class SaleInvoicesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async newSaleInvoice(req: Request, res: Response, next: NextFunction) { private async newSaleInvoice(
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 = await this.saleInvoiceService.createSaleInvoice( const storedSaleInvoice =
tenantId, await this.saleInvoiceApplication.createSaleInvoice(
saleInvoiceDTO, tenantId,
user saleInvoiceDTO,
); 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.',
@@ -285,14 +274,18 @@ export default class SaleInvoicesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async editSaleInvoice(req: Request, res: Response, next: NextFunction) { private async editSaleInvoice(
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.saleInvoiceService.editSaleInvoice( await this.saleInvoiceApplication.editSaleInvoice(
tenantId, tenantId,
saleInvoiceId, saleInvoiceId,
saleInvoiceOTD, saleInvoiceOTD,
@@ -313,12 +306,16 @@ export default class SaleInvoicesController extends BaseController {
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
async deliverSaleInvoice(req: Request, res: Response, next: NextFunction) { private async deliverSaleInvoice(
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.saleInvoiceService.deliverSaleInvoice( await this.saleInvoiceApplication.deliverSaleInvoice(
tenantId, tenantId,
saleInvoiceId, saleInvoiceId,
user user
@@ -338,13 +335,17 @@ export default class SaleInvoicesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async deleteSaleInvoice(req: Request, res: Response, next: NextFunction) { private async deleteSaleInvoice(
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.saleInvoiceService.deleteSaleInvoice( await this.saleInvoiceApplication.deleteSaleInvoice(
tenantId, tenantId,
saleInvoiceId, saleInvoiceId,
user user
@@ -364,12 +365,16 @@ 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.
*/ */
async getSaleInvoice(req: Request, res: Response, next: NextFunction) { private async getSaleInvoice(
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.saleInvoiceService.getSaleInvoice( const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice(
tenantId, tenantId,
saleInvoiceId, saleInvoiceId,
user user
@@ -384,7 +389,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.saleInvoicePdf.saleInvoicePdf( const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
tenantId, tenantId,
saleInvoice saleInvoice
); );
@@ -420,7 +425,7 @@ export default class SaleInvoicesController extends BaseController {
}; };
try { try {
const { salesInvoices, filterMeta, pagination } = const { salesInvoices, filterMeta, pagination } =
await this.saleInvoiceService.salesInvoicesList(tenantId, filter); await this.saleInvoiceApplication.getSaleInvoices(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices), sales_invoices: this.transfromToResponse(salesInvoices),
@@ -448,10 +453,11 @@ export default class SaleInvoicesController extends BaseController {
const { customerId } = this.matchedQueryData(req); const { customerId } = this.matchedQueryData(req);
try { try {
const salesInvoices = await this.saleInvoiceService.getPayableInvoices( const salesInvoices =
tenantId, await this.saleInvoiceApplication.getReceivableSaleInvoices(
customerId tenantId,
); customerId
);
return res.status(200).send({ return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices), sales_invoices: this.transfromToResponse(salesInvoices),
}); });
@@ -477,7 +483,7 @@ export default class SaleInvoicesController extends BaseController {
const writeoffDTO = this.matchedBodyData(req); const writeoffDTO = this.matchedBodyData(req);
try { try {
const saleInvoice = await this.saleInvoiceWriteoff.writeOff( const saleInvoice = await this.saleInvoiceApplication.writeOff(
tenantId, tenantId,
invoiceId, invoiceId,
writeoffDTO writeoffDTO
@@ -485,7 +491,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 writte-off successfully.', message: 'The given sale invoice has been written-off successfully.',
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -507,7 +513,7 @@ export default class SaleInvoicesController extends BaseController {
const { id: invoiceId } = req.params; const { id: invoiceId } = req.params;
try { try {
const saleInvoice = await this.saleInvoiceWriteoff.cancelWrittenoff( const saleInvoice = await this.saleInvoiceApplication.cancelWrittenoff(
tenantId, tenantId,
invoiceId invoiceId
); );
@@ -538,11 +544,12 @@ export default class SaleInvoicesController extends BaseController {
const invoiceNotifySmsDTO = this.matchedBodyData(req); const invoiceNotifySmsDTO = this.matchedBodyData(req);
try { try {
const saleInvoice = await this.saleInvoiceSmsNotify.notifyBySms( const saleInvoice =
tenantId, await this.saleInvoiceApplication.notifySaleInvoiceBySms(
invoiceId, tenantId,
invoiceNotifySmsDTO.notificationKey invoiceId,
); invoiceNotifySmsDTO.notificationKey
);
return res.status(200).send({ return res.status(200).send({
id: saleInvoice.id, id: saleInvoice.id,
message: message:
@@ -569,11 +576,12 @@ export default class SaleInvoicesController extends BaseController {
const smsDetailsDTO = this.matchedQueryData(req); const smsDetailsDTO = this.matchedQueryData(req);
try { try {
const invoiceSmsDetails = await this.saleInvoiceSmsNotify.smsDetails( const invoiceSmsDetails =
tenantId, await this.saleInvoiceApplication.getSaleInvoiceSmsDetails(
invoiceId, tenantId,
smsDetailsDTO invoiceId,
); smsDetailsDTO
);
return res.status(200).send({ return res.status(200).send({
data: invoiceSmsDetails, data: invoiceSmsDetails,
}); });
@@ -599,7 +607,7 @@ export default class SaleInvoicesController extends BaseController {
try { try {
const invoicePayments = const invoicePayments =
await this.invoicePaymentsSerivce.getInvoicePayments( await this.saleInvoiceApplication.getInvoicePayments(
tenantId, tenantId,
invoiceId invoiceId
); );

View File

@@ -2,34 +2,26 @@ 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()
saleReceiptService: SaleReceiptService; private saleReceiptsApplication: SaleReceiptApplication;
@Inject() @Inject()
saleReceiptsPdf: SaleReceiptsPdfService; private dynamicListService: DynamicListingService;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
saleReceiptSmsNotify: SaleReceiptNotifyBySms;
/** /**
* Router constructor. * Router constructor.
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -105,7 +97,7 @@ export default class SalesReceiptsController extends BaseController {
* Sales receipt validation schema. * Sales receipt validation schema.
* @return {Array} * @return {Array}
*/ */
get salesReceiptsValidationSchema() { private 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(),
@@ -146,14 +138,14 @@ export default class SalesReceiptsController extends BaseController {
/** /**
* Specific sale receipt validation schema. * Specific sale receipt validation schema.
*/ */
get specificReceiptValidationSchema() { private get specificReceiptValidationSchema() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* List sales receipts validation schema. * List sales receipts validation schema.
*/ */
get listSalesReceiptsValidationSchema() { private 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(),
@@ -170,16 +162,21 @@ export default class SalesReceiptsController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async newSaleReceipt(req: Request, res: Response, next: NextFunction) { private async newSaleReceipt(
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 = await this.saleReceiptService.createSaleReceipt( const storedSaleReceipt =
tenantId, await this.saleReceiptsApplication.createSaleReceipt(
saleReceiptDTO tenantId,
); 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.',
@@ -194,13 +191,20 @@ export default class SalesReceiptsController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async deleteSaleReceipt(req: Request, res: Response, next: NextFunction) { private async deleteSaleReceipt(
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.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId); await this.saleReceiptsApplication.deleteSaleReceipt(
tenantId,
saleReceiptId
);
return res.status(200).send({ return res.status(200).send({
id: saleReceiptId, id: saleReceiptId,
@@ -217,14 +221,18 @@ export default class SalesReceiptsController extends BaseController {
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
async editSaleReceipt(req: Request, res: Response, next: NextFunction) { private async editSaleReceipt(
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.saleReceiptService.editSaleReceipt( await this.saleReceiptsApplication.editSaleReceipt(
tenantId, tenantId,
saleReceiptId, saleReceiptId,
saleReceipt saleReceipt
@@ -244,13 +252,20 @@ export default class SalesReceiptsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
async closeSaleReceipt(req: Request, res: Response, next: NextFunction) { private async closeSaleReceipt(
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.saleReceiptService.closeSaleReceipt(tenantId, saleReceiptId); await this.saleReceiptsApplication.closeSaleReceipt(
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.',
@@ -265,7 +280,11 @@ export default class SalesReceiptsController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async getSalesReceipts(req: Request, res: Response, next: NextFunction) { private async getSalesReceipts(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
sortOrder: 'desc', sortOrder: 'desc',
@@ -274,10 +293,9 @@ 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.saleReceiptService.salesReceiptsList(tenantId, filter); await this.saleReceiptsApplication.getSaleReceipts(tenantId, filter);
const response = this.transfromToResponse({ const response = this.transfromToResponse({
data, data,
@@ -301,11 +319,10 @@ export default class SalesReceiptsController extends BaseController {
const { tenantId } = req; const { tenantId } = req;
try { try {
const saleReceipt = await this.saleReceiptService.getSaleReceipt( const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
tenantId, tenantId,
saleReceiptId saleReceiptId
); );
res.format({ res.format({
'application/json': () => { 'application/json': () => {
return res return res
@@ -313,10 +330,11 @@ export default class SalesReceiptsController extends BaseController {
.send(this.transfromToResponse({ saleReceipt })); .send(this.transfromToResponse({ saleReceipt }));
}, },
'application/pdf': async () => { 'application/pdf': async () => {
const pdfContent = await this.saleReceiptsPdf.saleReceiptPdf( const pdfContent =
tenantId, await this.saleReceiptsApplication.getSaleReceiptPdf(
saleReceipt tenantId,
); saleReceipt
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
@@ -344,10 +362,11 @@ export default class SalesReceiptsController extends BaseController {
const { id: receiptId } = req.params; const { id: receiptId } = req.params;
try { try {
const saleReceipt = await this.saleReceiptSmsNotify.notifyBySms( const saleReceipt =
tenantId, await this.saleReceiptsApplication.saleReceiptNotifyBySms(
receiptId tenantId,
); receiptId
);
return res.status(200).send({ return res.status(200).send({
id: saleReceipt.id, id: saleReceipt.id,
message: message:
@@ -373,10 +392,11 @@ export default class SalesReceiptsController extends BaseController {
const { id: receiptId } = req.params; const { id: receiptId } = req.params;
try { try {
const smsDetails = await this.saleReceiptSmsNotify.smsDetails( const smsDetails =
tenantId, await this.saleReceiptsApplication.getSaleReceiptSmsDetails(
receiptId tenantId,
); 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 SalesInvoices from './SalesInvoices'
import PaymentReceives from './PaymentReceives';
import CreditNotes from './CreditNotes'; import CreditNotes from './CreditNotes';
import PaymentReceives from './PaymentReceives';
@Service() @Service()
export default class SalesController { export default class SalesController {
/** /**

View File

@@ -1,9 +1,12 @@
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'; 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
@@ -97,7 +100,7 @@ module.exports = {
jwtSecret: process.env.JWT_SECRET, jwtSecret: process.env.JWT_SECRET,
/** /**
* *
*/ */
resetPasswordSeconds: 600, resetPasswordSeconds: 600,
@@ -130,9 +133,9 @@ module.exports = {
blockDuration: 60 * 15, blockDuration: 60 * 15,
}, },
requests: { requests: {
points: 60, points: API_RATE_LIMIT[0] ? toInteger(API_RATE_LIMIT[0]) : 120,
duration: 60, duration: API_RATE_LIMIT[1] ? toInteger(API_RATE_LIMIT[1]) : 60,
blockDuration: 60 * 10, blockDuration: API_RATE_LIMIT[2] ? toInteger(API_RATE_LIMIT[2]) : 60 * 10,
}, },
}, },

View File

@@ -58,6 +58,7 @@ export interface IAccountTransaction {
date: string | Date; date: string | Date;
referenceType: string; referenceType: string;
referenceTypeFormatted: string;
referenceId: number; referenceId: number;
referenceNumber?: string; referenceNumber?: string;

View File

@@ -1,7 +1,7 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { IDynamicListFilterDTO } from './DynamicFilter'; import { IDynamicListFilterDTO } from './DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry'; import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IBillLandedCost } from './LandedCost'; import { IBillLandedCost } from './LandedCost';
export interface IBillDTO { export interface IBillDTO {
vendorId: number; vendorId: number;
billNumber: string; billNumber: string;
@@ -99,17 +99,17 @@ export interface IBillCreatedPayload {
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface IBillCreatingPayload{ export interface IBillCreatingPayload {
tenantId: number; tenantId: number;
billDTO: IBillDTO; billDTO: IBillDTO;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface IBillEditingPayload { export interface IBillEditingPayload {
tenantId: number; tenantId: number;
oldBill: IBill; oldBill: IBill;
billDTO: IBillEditDTO; billDTO: IBillEditDTO;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface IBillEditedPayload { export interface IBillEditedPayload {
tenantId: number; tenantId: number;
@@ -129,7 +129,7 @@ export interface IBIllEventDeletedPayload {
export interface IBillEventDeletingPayload { export interface IBillEventDeletingPayload {
tenantId: number; tenantId: number;
oldBill: IBill; oldBill: IBill;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export enum BillAction { export enum BillAction {
Create = 'Create', Create = 'Create',
@@ -138,3 +138,16 @@ export enum BillAction {
View = 'View', View = 'View',
NotifyBySms = 'NotifyBySms', NotifyBySms = 'NotifyBySms',
} }
export interface IBillOpeningPayload {
trx: Knex.Transaction;
tenantId: number;
oldBill: IBill;
}
export interface IBillOpenedPayload {
trx: Knex.Transaction;
bill: IBill;
oldBill: IBill;
tenantId: number;
}

View File

@@ -41,6 +41,8 @@ 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

@@ -1,6 +1,5 @@
import { ISystemUser } from '@/interfaces';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { pick } from 'lodash'; import { ISystemUser } from '@/interfaces';
import { ILedgerEntry } from './Ledger'; import { ILedgerEntry } from './Ledger';
import { ISaleInvoice } from './SaleInvoice'; import { ISaleInvoice } from './SaleInvoice';

View File

@@ -156,6 +156,7 @@ export interface ISaleInvoiceEventDeliveredPayload {
tenantId: number; tenantId: number;
saleInvoiceId: number; saleInvoiceId: number;
saleInvoice: ISaleInvoice; saleInvoice: ISaleInvoice;
trx: Knex.Transaction;
} }
export interface ISaleInvoiceDeliveringPayload { export interface ISaleInvoiceDeliveringPayload {

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(SalesInvoicesCost); const salesInvoicesCost = Container.get(SaleInvoicesCost);
try { try {
await salesInvoicesCost.writeCostLotsGLEntries(tenantId, startingDate); await salesInvoicesCost.writeCostLotsGLEntries(tenantId, startingDate);

View File

@@ -1,17 +1,16 @@
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import ItemSubscriber from '@/subscribers/Items/ItemSubscriber';
import InventoryAdjustmentsSubscriber from '@/subscribers/Inventory/InventoryAdjustment'; import InventoryAdjustmentsSubscriber from '@/subscribers/Inventory/InventoryAdjustment';
import BillWriteInventoryTransactionsSubscriber from '@/subscribers/Bills/WriteInventoryTransactions'; import BillWriteInventoryTransactionsSubscriber from '@/subscribers/Bills/WriteInventoryTransactions';
import PaymentSyncBillBalance from '@/subscribers/PaymentMades/PaymentSyncBillBalance'; import PaymentSyncBillBalance from '@/subscribers/PaymentMades/PaymentSyncBillBalance';
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';
@@ -35,7 +34,7 @@ import PurgeAuthorizedUserOnceRoleMutate from '@/services/Roles/PurgeAuthorizedU
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/SaleInvoiceWriteoffSubscriber'; import SaleInvoiceWriteoffSubscriber from '@/services/Sales/Invoices/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,7 +65,6 @@ 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';
@@ -88,7 +86,6 @@ export default () => {
export const susbcribers = () => { export const susbcribers = () => {
return [ return [
ItemSubscriber,
InventoryAdjustmentsSubscriber, InventoryAdjustmentsSubscriber,
BillWriteInventoryTransactionsSubscriber, BillWriteInventoryTransactionsSubscriber,
PaymentSyncBillBalance, PaymentSyncBillBalance,

View File

@@ -2,8 +2,11 @@ import { Model, raw } from 'objection';
import moment from 'moment'; import moment from 'moment';
import { isEmpty, castArray } from 'lodash'; import { isEmpty, castArray } from 'lodash';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { getTransactionTypeLabel } from '@/utils/transactions-types';
export default class AccountTransaction extends TenantModel { export default class AccountTransaction extends TenantModel {
referenceType: string;
/** /**
* Table name * Table name
*/ */
@@ -30,40 +33,7 @@ export default class AccountTransaction extends TenantModel {
* @return {string} * @return {string}
*/ */
get referenceTypeFormatted() { get referenceTypeFormatted() {
return AccountTransaction.getReferenceTypeFormatted(this.referenceType); return getTransactionTypeLabel(this.referenceType);
}
/**
* Reference type formatted.
*/
static getReferenceTypeFormatted(referenceType) {
const mapped = {
SaleInvoice: 'Sale invoice',
SaleReceipt: 'Sale receipt',
PaymentReceive: 'Payment receive',
Bill: 'Bill',
BillPayment: 'Payment made',
VendorOpeningBalance: 'Vendor opening balance',
CustomerOpeningBalance: 'Customer opening balance',
InventoryAdjustment: 'Inventory adjustment',
ManualJournal: 'Manual journal',
Journal: 'Manual journal',
Expense: 'Expense',
OwnerContribution: 'Owner contribution',
TransferToAccount: 'Transfer to account',
TransferFromAccount: 'Transfer from account',
OtherIncome: 'Other income',
OtherExpense: 'Other expense',
OwnerDrawing: 'Owner drawing',
InvoiceWriteOff: 'Invoice write-off',
CreditNote: 'transaction_type.credit_note',
VendorCredit: 'transaction_type.vendor_credit',
RefundCreditNote: 'transaction_type.refund_credit_note',
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
};
return mapped[referenceType] || '';
} }
/** /**

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/constants'; import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/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

@@ -1,9 +1,13 @@
import { Model, raw } from 'objection'; import { Model, raw } from 'objection';
import { castArray, isEmpty } from 'lodash'; import { castArray } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { getTransactionTypeLabel } from '@/utils/transactions-types';
export default class InventoryTransaction extends TenantModel { export default class InventoryTransaction extends TenantModel {
transactionId: number;
transactionType: string;
/** /**
* Table name * Table name
*/ */
@@ -23,27 +27,7 @@ export default class InventoryTransaction extends TenantModel {
* @return {string} * @return {string}
*/ */
get transcationTypeFormatted() { get transcationTypeFormatted() {
return InventoryTransaction.getReferenceTypeFormatted(this.transactionType); return getTransactionTypeLabel(this.transactionType);
}
/**
* Reference type formatted.
*/
static getReferenceTypeFormatted(referenceType) {
const mapped = {
SaleInvoice: 'Sale invoice',
SaleReceipt: 'Sale receipt',
PaymentReceive: 'Payment receive',
Bill: 'Bill',
BillPayment: 'Payment made',
VendorOpeningBalance: 'Vendor opening balance',
CustomerOpeningBalance: 'Customer opening balance',
InventoryAdjustment: 'Inventory adjustment',
ManualJournal: 'Manual journal',
Journal: 'Manual journal',
LandedCost: 'transaction_type.landed_cost',
};
return mapped[referenceType] || '';
} }
/** /**

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/constants'; import { DEFAULT_VIEWS } from '@/services/Sales/Invoices/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

@@ -21,6 +21,8 @@ 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

@@ -46,7 +46,7 @@ export default class AccountTransactionTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
public transactionTypeFormatted(transaction: IAccountTransaction) { public transactionTypeFormatted(transaction: IAccountTransaction) {
return transaction.referenceTypeFormatted; return this.context.i18n.__(transaction.referenceTypeFormatted);
} }
/** /**

View File

@@ -23,15 +23,6 @@ export class GetAccounts {
@Inject() @Inject()
private transformer: TransformerInjectable; private transformer: TransformerInjectable;
/**
* Parsees accounts list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/** /**
* Retrieve accounts datatable list. * Retrieve accounts datatable list.
* @param {number} tenantId * @param {number} tenantId
@@ -75,4 +66,13 @@ export class GetAccounts {
filterMeta: dynamicList.getResponseMeta(), filterMeta: dynamicList.getResponseMeta(),
}; };
}; };
/**
* Parsees accounts list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
} }

View File

@@ -1,9 +0,0 @@
export class AccountsReceivableRepository {
findOrCreateAccount = (currencyCode?: string) => {
};
}

View File

@@ -8,6 +8,7 @@ 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

@@ -14,8 +14,8 @@ export class ValidateBranchExistance {
/** /**
* Validate transaction branch id when the feature is active. * Validate transaction branch id when the feature is active.
* @param {number} tenantId * @param {number} tenantId
* @param {number} branchId * @param {number} branchId
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public validateTransactionBranchWhenActive = async ( public validateTransactionBranchWhenActive = async (
@@ -32,18 +32,16 @@ export class ValidateBranchExistance {
/** /**
* Validate transaction branch id existance. * Validate transaction branch id existance.
* @param {number} tenantId * @param {number} tenantId
* @param {number} branchId * @param {number} branchId
* @return {Promise<void>} * @return {Promise<void>}
*/ */
public validateTransactionBranch = async ( public validateTransactionBranch = async (
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);
}; };
@@ -62,7 +60,10 @@ export class ValidateBranchExistance {
* @param tenantId * @param tenantId
* @param branchId * @param branchId
*/ */
public validateBranchExistance = async (tenantId: number, branchId: number) => { public validateBranchExistance = async (
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 {} * @returns {Partial<ILedgerEntry>}
*/ */
private getCommonEntry = (cashflowTransaction: ICashflowTransaction) => { private getCommonEntry = (cashflowTransaction: ICashflowTransaction) => {
const { entries, ...transaction } = cashflowTransaction; const { entries, ...transaction } = cashflowTransaction;
@@ -41,7 +41,9 @@ export default class CashflowTransactionJournalEntries {
), ),
transactionId: transaction.id, transactionId: transaction.id,
transactionNumber: transaction.transactionNumber, transactionNumber: transaction.transactionNumber,
referenceNo: transaction.referenceNo, referenceNumber: transaction.referenceNo,
note: transaction.description,
branchId: cashflowTransaction.branchId, branchId: cashflowTransaction.branchId,
userId: cashflowTransaction.userId, userId: cashflowTransaction.userId,
@@ -76,9 +78,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 = (
@@ -102,10 +104,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 * @returns {ILedgerEntry[]}
*/ */
private getJournalEntries = ( private getJournalEntries = (
cashflowTransaction: ICashflowTransaction cashflowTransaction: ICashflowTransaction
@@ -118,7 +120,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) => {
@@ -130,6 +132,7 @@ 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,
@@ -153,6 +156,7 @@ 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

@@ -16,16 +16,16 @@ import BaseCreditNotes from './CreditNotes';
@Service() @Service()
export default class CreateCreditNote extends BaseCreditNotes { export default class CreateCreditNote extends BaseCreditNotes {
@Inject() @Inject()
uow: UnitOfWork; private uow: UnitOfWork;
@Inject() @Inject()
itemsEntriesService: ItemsEntriesService; private itemsEntriesService: ItemsEntriesService;
@Inject() @Inject()
tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject() @Inject()
eventPublisher: EventPublisher; private eventPublisher: EventPublisher;
/** /**
* Creates a new credit note. * Creates a new credit note.

View File

@@ -1,5 +1,4 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { import {
IApplyCreditToInvoicesCreatedPayload, IApplyCreditToInvoicesCreatedPayload,
@@ -10,15 +9,12 @@ import CreditNoteApplySyncInvoicesCreditedAmount from './CreditNoteApplySyncInvo
@Service() @Service()
export default class CreditNoteApplySyncInvoicesCreditedAmountSubscriber { export default class CreditNoteApplySyncInvoicesCreditedAmountSubscriber {
@Inject() @Inject()
tenancy: HasTenancyService; private syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount;
@Inject()
syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount;
/** /**
* Attaches events with handlers. * Attaches events with handlers.
*/ */
attach(bus) { public attach(bus) {
bus.subscribe( bus.subscribe(
events.creditNote.onApplyToInvoicesCreated, events.creditNote.onApplyToInvoicesCreated,
this.incrementAppliedInvoicesOnceCreditCreated this.incrementAppliedInvoicesOnceCreditCreated

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,27 +8,31 @@ 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 events from '@/subscribers/events'; import { PaymentReceiveValidators } from '../Sales/PaymentReceives/PaymentReceiveValidators';
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('PaymentReceives') @Inject()
paymentReceive: PaymentReceiveService; private tenancy: HasTenancyService;
@Inject() @Inject()
uow: UnitOfWork; private paymentReceiveValidators: PaymentReceiveValidators;
@Inject() @Inject()
eventPublisher: EventPublisher; private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Apply credit note to the given invoices. * Apply credit note to the given invoices.
@@ -50,7 +54,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.paymentReceive.validateInvoicesIDsExistance( await this.paymentReceiveValidators.validateInvoicesIDsExistance(
tenantId, tenantId,
creditNote.customerId, creditNote.customerId,
applyCreditToInvoicesDTO.entries applyCreditToInvoicesDTO.entries

View File

@@ -15,7 +15,7 @@ export default class CreditNoteInventoryTransactionsSubscriber {
/** /**
* Attaches events with publisher. * Attaches events with publisher.
*/ */
attach(bus) { public attach(bus) {
bus.subscribe( bus.subscribe(
events.creditNote.onCreated, events.creditNote.onCreated,
this.writeInventoryTranscationsOnceCreated this.writeInventoryTranscationsOnceCreated
@@ -37,6 +37,7 @@ export default class CreditNoteInventoryTransactionsSubscriber {
/** /**
* Writes inventory transactions once credit note created. * Writes inventory transactions once credit note created.
* @param {ICreditNoteCreatedPayload} payload - * @param {ICreditNoteCreatedPayload} payload -
* @returns {Promise<void>}
*/ */
public writeInventoryTranscationsOnceCreated = async ({ public writeInventoryTranscationsOnceCreated = async ({
tenantId, tenantId,
@@ -44,9 +45,8 @@ export default class CreditNoteInventoryTransactionsSubscriber {
trx, trx,
}: ICreditNoteCreatedPayload) => { }: ICreditNoteCreatedPayload) => {
// Can't continue if the credit note is open yet. // Can't continue if the credit note is open yet.
if (!creditNote.isOpen) { if (!creditNote.isOpen) return;
return;
}
await this.inventoryTransactions.createInventoryTransactions( await this.inventoryTransactions.createInventoryTransactions(
tenantId, tenantId,
creditNote, creditNote,
@@ -57,6 +57,7 @@ export default class CreditNoteInventoryTransactionsSubscriber {
/** /**
* Rewrites inventory transactions once credit note edited. * Rewrites inventory transactions once credit note edited.
* @param {ICreditNoteEditedPayload} payload - * @param {ICreditNoteEditedPayload} payload -
* @returns {Promise<void>}
*/ */
public rewriteInventoryTransactionsOnceEdited = async ({ public rewriteInventoryTransactionsOnceEdited = async ({
tenantId, tenantId,
@@ -65,9 +66,8 @@ export default class CreditNoteInventoryTransactionsSubscriber {
trx, trx,
}: ICreditNoteEditedPayload) => { }: ICreditNoteEditedPayload) => {
// Can't continue if the credit note is open yet. // Can't continue if the credit note is open yet.
if (!creditNote.isOpen) { if (!creditNote.isOpen) return;
return;
}
await this.inventoryTransactions.editInventoryTransactions( await this.inventoryTransactions.editInventoryTransactions(
tenantId, tenantId,
creditNoteId, creditNoteId,
@@ -87,9 +87,8 @@ export default class CreditNoteInventoryTransactionsSubscriber {
trx, trx,
}: ICreditNoteDeletedPayload) => { }: ICreditNoteDeletedPayload) => {
// Can't continue if the credit note is open yet. // Can't continue if the credit note is open yet.
if (!oldCreditNote.isOpen) { if (!oldCreditNote.isOpen) return;
return;
}
await this.inventoryTransactions.deleteInventoryTransactions( await this.inventoryTransactions.deleteInventoryTransactions(
tenantId, tenantId,
creditNoteId, creditNoteId,

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('PaymentReceives') @Inject()
paymentReceive: PaymentReceiveService; private uow: UnitOfWork;
@Inject() @Inject()
uow: UnitOfWork; private eventPublisher: EventPublisher;
@Inject() @Inject()
eventPublisher: EventPublisher; private tenancy: HasTenancyService;
/** /**
* Apply credit note to the given invoices. * Apply credit note to the given invoices.

View File

@@ -21,7 +21,7 @@ export class ExpensesWriteGLSubscriber {
* Attaches events with handlers. * Attaches events with handlers.
* @param bus * @param bus
*/ */
attach(bus) { public attach(bus) {
bus.subscribe( bus.subscribe(
events.expenses.onCreated, events.expenses.onCreated,
this.handleWriteGLEntriesOnceCreated this.handleWriteGLEntriesOnceCreated

View File

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

View File

@@ -27,7 +27,7 @@ export class ItemInvoicesTransactionsTransformer extends Transformer {
} }
/** /**
* * Formatted invoice date.
* @param item * @param item
* @returns * @returns
*/ */
@@ -36,16 +36,17 @@ 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 * @returns {string}
*/ */
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()
tenancy: TenancyService; private 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.
*/ */
setItemsEntriesDefaultAccounts(tenantId: number) { public setItemsEntriesDefaultAccounts(tenantId: number) {
return async (entries: IItemEntry[]) => { return async (entries: IItemEntry[]) => {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);
@@ -258,10 +258,10 @@ export default class ItemsEntriesService {
/** /**
* Retrieve the total items entries. * Retrieve the total items entries.
* @param entries * @param entries
* @returns * @returns
*/ */
getTotalItemsEntries(entries: ItemEntry[]): number { public getTotalItemsEntries(entries: ItemEntry[]): number {
return sumBy(entries, (e) => ItemEntry.calcAmount(e)); return sumBy(entries, (e) => ItemEntry.calcAmount(e));
} }
} }

View File

@@ -48,6 +48,7 @@ export class ManualJournalWriteGLSubscriber {
/** /**
* Handle manual journal created event. * Handle manual journal created event.
* @param {IManualJournalEventCreatedPayload} payload - * @param {IManualJournalEventCreatedPayload} payload -
* @returns {Promise<void>}
*/ */
private handleWriteJournalEntriesOnCreated = async ({ private handleWriteJournalEntriesOnCreated = async ({
tenantId, tenantId,
@@ -55,18 +56,19 @@ export class ManualJournalWriteGLSubscriber {
trx, trx,
}: IManualJournalEventCreatedPayload) => { }: IManualJournalEventCreatedPayload) => {
// Ingore writing manual journal journal entries in case was not published. // Ingore writing manual journal journal entries in case was not published.
if (manualJournal.publishedAt) { if (!manualJournal.publishedAt) return;
await this.manualJournalGLEntries.createManualJournalGLEntries(
tenantId, await this.manualJournalGLEntries.createManualJournalGLEntries(
manualJournal.id, tenantId,
trx manualJournal.id,
); trx
} );
}; };
/** /**
* Handles the manual journal next number increment once the journal be created. * Handles the manual journal next number increment once the journal be created.
* @param {IManualJournalEventCreatedPayload} payload - * @param {IManualJournalEventCreatedPayload} payload -
* @return {Promise<void>}
*/ */
private handleJournalNumberIncrement = async ({ private handleJournalNumberIncrement = async ({
tenantId, tenantId,
@@ -77,6 +79,7 @@ export class ManualJournalWriteGLSubscriber {
/** /**
* Handle manual journal edited event. * Handle manual journal edited event.
* @param {IManualJournalEventEditedPayload} * @param {IManualJournalEventEditedPayload}
* @return {Promise<void>}
*/ */
private handleRewriteJournalEntriesOnEdited = async ({ private handleRewriteJournalEntriesOnEdited = async ({
tenantId, tenantId,
@@ -96,6 +99,7 @@ export class ManualJournalWriteGLSubscriber {
/** /**
* Handles writing journal entries once the manula journal publish. * Handles writing journal entries once the manula journal publish.
* @param {IManualJournalEventPublishedPayload} payload - * @param {IManualJournalEventPublishedPayload} payload -
* @return {Promise<void>}
*/ */
private handleWriteJournalEntriesOnPublished = async ({ private handleWriteJournalEntriesOnPublished = async ({
tenantId, tenantId,

View File

@@ -0,0 +1,48 @@
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 * @returns {string}
*/ */
protected formattedPaymentDate = (entry): string => { protected formattedPaymentDate = (entry): string => {
return this.formatDate(entry.payment.paymentDate); return this.formatDate(entry.payment.paymentDate);

View File

@@ -0,0 +1,278 @@
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

@@ -1,713 +0,0 @@
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 associated 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

@@ -0,0 +1,109 @@
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);
}
}

View File

@@ -5,13 +5,10 @@ import { IBill, IBillPayment, IBillReceivePageEntry } from '@/interfaces';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
/**
* Bill payments edit and create pages services.
*/
@Service() @Service()
export default class BillPaymentsPages { export default class BillPaymentsPages {
@Inject() @Inject()
tenancy: TenancyService; private tenancy: TenancyService;
/** /**
* Retrieve bill payment with associated metadata. * Retrieve bill payment with associated metadata.

View File

@@ -0,0 +1,37 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import { sumBy } from 'lodash';
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { formatDateFields } from '@/utils';
@Service()
export class CommandBillPaymentDTOTransformer {
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
/**
* Transforms create/edit DTO to model.
* @param {number} tenantId
* @param {IBillPaymentDTO} billPaymentDTO - Bill payment.
* @param {IBillPayment} oldBillPayment - Old bill payment.
* @return {Promise<IBillPayment>}
*/
public 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);
}
}

View File

@@ -0,0 +1,124 @@
import { Knex } from 'knex';
import events from '@/subscribers/events';
import {
IBillPaymentDTO,
IBillPayment,
IBillPaymentEventCreatedPayload,
IBillPaymentCreatingPayload,
} from '@/interfaces';
import { TenantMetadata } from '@/system/models';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { BillPaymentValidators } from './BillPaymentValidators';
import { CommandBillPaymentDTOTransformer } from './CommandBillPaymentDTOTransformer';
@Service()
export class CreateBillPayment {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validators: BillPaymentValidators;
@Inject()
private commandTransformerDTO: CommandBillPaymentDTOTransformer;
/**
* 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.commandTransformerDTO.transformDTOToModel(
tenantId,
billPaymentDTO,
vendor
);
// Validate the payment account existance and type.
const paymentAccount = await this.validators.getPaymentAccountOrThrowError(
tenantId,
billPaymentObj.paymentAccountId
);
// Validate the payment number uniquiness.
if (billPaymentObj.paymentNumber) {
await this.validators.validatePaymentNumber(
tenantId,
billPaymentObj.paymentNumber
);
}
// Validates the bills existance and associated to the given vendor.
await this.validators.validateBillsExistance(
tenantId,
billPaymentObj.entries,
billPaymentDTO.vendorId
);
// Validates the bills due payment amount.
await this.validators.validateBillsDueAmount(
tenantId,
billPaymentObj.entries
);
// Validates the withdrawal account currency code.
this.validators.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;
});
}
}

View File

@@ -0,0 +1,71 @@
import { Knex } from 'knex';
import UnitOfWork from '@/services/UnitOfWork';
import { BillPaymentValidators } from './BillPaymentValidators';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import {
IBillPaymentDeletingPayload,
IBillPaymentEventDeletedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
@Service()
export class DeleteBillPayment {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validators: BillPaymentValidators;
/**
* 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 BillPayment.query()
.withGraphFetched('entries')
.findById(billPaymentId);
// Validates the bill payment existance.
this.validators.validateBillPaymentExistance(oldBillPayment);
// 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);
});
}
}

View File

@@ -0,0 +1,146 @@
import { Inject, Service } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { BillPaymentValidators } from './BillPaymentValidators';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import {
IBillPayment,
IBillPaymentEditingPayload,
IBillPaymentEventEditedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { Knex } from 'knex';
import UnitOfWork from '@/services/UnitOfWork';
import { CommandBillPaymentDTOTransformer } from './CommandBillPaymentDTOTransformer';
import { TenantMetadata } from '@/system/models';
@Service()
export class EditBillPayment {
@Inject()
private validators: BillPaymentValidators;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private transformer: CommandBillPaymentDTOTransformer;
/**
* 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 BillPayment.query().findById(billPaymentId);
// Validates the bill payment existance.
this.validators.validateBillPaymentExistance(oldBillPayment);
//
const vendor = await Contact.query()
.modify('vendor')
.findById(billPaymentDTO.vendorId)
.throwIfNotFound();
// Transform bill payment DTO to model object.
const billPaymentObj = await this.transformer.transformDTOToModel(
tenantId,
billPaymentDTO,
vendor,
oldBillPayment
);
// Validate vendor not modified.
this.validators.validateVendorNotModified(billPaymentDTO, oldBillPayment);
// Validate the payment account existance and type.
const paymentAccount = await this.validators.getPaymentAccountOrThrowError(
tenantId,
billPaymentObj.paymentAccountId
);
// Validate the items entries IDs existance on the storage.
await this.validators.validateEntriesIdsExistance(
tenantId,
billPaymentId,
billPaymentObj.entries
);
// Validate the bills existance and associated to the given vendor.
await this.validators.validateBillsExistance(
tenantId,
billPaymentObj.entries,
billPaymentDTO.vendorId
);
// Validates the bills due payment amount.
await this.validators.validateBillsDueAmount(
tenantId,
billPaymentObj.entries,
oldBillPayment.entries
);
// Validate the payment number uniquiness.
if (billPaymentObj.paymentNumber) {
await this.validators.validatePaymentNumber(
tenantId,
billPaymentObj.paymentNumber,
billPaymentId
);
}
// Validates the withdrawal account currency code.
this.validators.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;
});
}
}

View File

@@ -0,0 +1,51 @@
import { IBillPayment } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ERRORS } from './constants';
import { ServiceError } from '@/exceptions';
import { BillPaymentTransformer } from './BillPaymentTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { BillsValidators } from '../Bills/BillsValidators';
import { BillPaymentValidators } from './BillPaymentValidators';
@Service()
export class GetBillPayment {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private validators: BillPaymentValidators;
/**
* 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);
// Validates the bill payment existance.
this.validators.validateBillPaymentExistance(billPayment);
return this.transformer.transform(
tenantId,
billPayment,
new BillPaymentTransformer()
);
}
}

View File

@@ -0,0 +1,74 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import {
IBillPayment,
IBillPaymentsFilter,
IPaginationMeta,
IFilterMeta,
} from '@/interfaces';
import { BillPaymentTransformer } from './BillPaymentTransformer';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetBillPayments {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve bill payment paginted and filterable list.
* @param {number} tenantId
* @param {IBillPaymentsFilter} billPaymentsFilter
*/
public async getBillPayments(
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(),
};
}
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,32 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { BillPaymentValidators } from './BillPaymentValidators';
@Service()
export class GetPaymentBills {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private validators: BillPaymentValidators;
/**
* Retrieve payment made associated bills.
* @param {number} tenantId -
* @param {number} billPaymentId -
*/
public async getPaymentBills(tenantId: number, billPaymentId: number) {
const { Bill, BillPayment } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query().findById(billPaymentId);
// Validates the bill payment existance.
this.validators.validateBillPaymentExistance(billPayment);
const paymentBillsIds = billPayment.entries.map((entry) => entry.id);
const bills = await Bill.query().whereIn('id', paymentBillsIds);
return bills;
}
}

View File

@@ -1,751 +0,0 @@
import { omit, sumBy } from 'lodash';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import { Knex } from 'knex';
import composeAsync from 'async/compose';
import events from '@/subscribers/events';
import InventoryService from '@/services/Inventory/Inventory';
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { formatDateFields, transformToMap } from 'utils';
import {
IBillDTO,
IBill,
ISystemUser,
IBillEditDTO,
IPaginationMeta,
IFilterMeta,
IBillsFilter,
IBillsService,
IItemEntry,
IItemEntryDTO,
IBillCreatedPayload,
IBillEditedPayload,
IBIllEventDeletedPayload,
IBillEventDeletingPayload,
IBillEditingPayload,
IBillCreatingPayload,
IVendor,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import { ERRORS } from './constants';
import EntriesService from '@/services/Entries';
import { PurchaseInvoiceTransformer } from './PurchaseInvoices/PurchaseInvoiceTransformer';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
/**
* Vendor bills services.
* @service
*/
@Service('Bills')
export default class BillsService
extends SalesInvoicesCost
implements IBillsService
{
@Inject()
inventoryService: InventoryService;
@Inject()
tenancy: TenancyService;
@Inject()
eventPublisher: EventPublisher;
@Inject('logger')
logger: any;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
itemsEntriesService: ItemsEntriesService;
@Inject()
journalPosterService: JournalPosterService;
@Inject()
entriesService: EntriesService;
@Inject()
transformer: TransformerInjectable;
@Inject()
uow: UnitOfWork;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
/**
* Validates the given bill existance.
* @async
* @param {number} tenantId -
* @param {number} billId -
*/
public async getBillOrThrowError(tenantId: number, billId: number) {
const { Bill } = this.tenancy.models(tenantId);
const foundBill = await Bill.query()
.findById(billId)
.withGraphFetched('entries');
if (!foundBill) {
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
}
return foundBill;
}
/**
* Validates the bill number existance.
* @async
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async validateBillNumberExists(
tenantId: number,
billNumber: string,
notBillId?: number
) {
const { Bill } = this.tenancy.models(tenantId);
const foundBills = await Bill.query()
.where('bill_number', billNumber)
.onBuild((builder) => {
if (notBillId) {
builder.whereNot('id', notBillId);
}
});
if (foundBills.length > 0) {
throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS);
}
}
/**
* Validate the bill has no payment entries.
* @param {number} tenantId
* @param {number} billId - Bill id.
*/
private async validateBillHasNoEntries(tenantId, billId: number) {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
// Retireve the bill associate payment made entries.
const entries = await BillPaymentEntry.query().where('bill_id', billId);
if (entries.length > 0) {
throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES);
}
return entries;
}
/**
* Validate the bill number require.
* @param {string} billNo -
*/
private validateBillNoRequire(billNo: string) {
if (!billNo) {
throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED);
}
}
/**
* Validate bill transaction has no associated allocated landed cost transactions.
* @param {number} tenantId
* @param {number} billId
*/
private async validateBillHasNoLandedCost(tenantId: number, billId: number) {
const { BillLandedCost } = this.tenancy.models(tenantId);
const billLandedCosts = await BillLandedCost.query().where(
'billId',
billId
);
if (billLandedCosts.length > 0) {
throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_LANDED_COSTS);
}
}
/**
* Validate transaction entries that have landed cost type should not be
* inventory items.
* @param {number} tenantId -
* @param {IItemEntryDTO[]} newEntriesDTO -
*/
public async validateCostEntriesShouldBeInventoryItems(
tenantId: number,
newEntriesDTO: IItemEntryDTO[]
) {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = newEntriesDTO.map((e) => e.itemId);
const entriesItems = await Item.query().whereIn('id', entriesItemsIds);
const entriesItemsById = transformToMap(entriesItems, 'id');
// Filter the landed cost entries that not associated with inventory item.
const nonInventoryHasCost = newEntriesDTO.filter((entry) => {
const item = entriesItemsById.get(entry.itemId);
return entry.landedCost && item.type !== 'inventory';
});
if (nonInventoryHasCost.length > 0) {
throw new ServiceError(
ERRORS.LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS
);
}
}
/**
* Sets the default cost account to the bill entries.
*/
private setBillEntriesDefaultAccounts(tenantId: number) {
return async (entries: IItemEntry[]) => {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
const items = await Item.query().whereIn('id', entriesItemsIds);
return entries.map((entry) => {
const item = items.find((i) => i.id === entry.itemId);
return {
...entry,
...(item.type !== 'inventory' && {
costAccountId: entry.costAccountId || item.costAccountId,
}),
};
});
};
}
/**
* Retrieve the bill entries total.
* @param {IItemEntry[]} entries
* @returns {number}
*/
private getBillEntriesTotal(tenantId: number, entries: IItemEntry[]): number {
const { ItemEntry } = this.tenancy.models(tenantId);
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
}
/**
* Retrieve the bill landed cost amount.
* @param {IBillDTO} billDTO
* @returns {number}
*/
private getBillLandedCostAmount(tenantId: number, billDTO: IBillDTO): number {
const costEntries = billDTO.entries.filter((entry) => entry.landedCost);
return this.getBillEntriesTotal(tenantId, costEntries);
}
/**
* Converts create bill DTO to model.
* @param {number} tenantId
* @param {IBillDTO} billDTO
* @param {IBill} oldBill
* @returns {IBill}
*/
private async billDTOToModel(
tenantId: number,
billDTO: IBillDTO,
vendor: IVendor,
authorizedUser: ISystemUser,
oldBill?: IBill
) {
const { ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e));
// Retrieve the landed cost amount from landed cost entries.
const landedCostAmount = this.getBillLandedCostAmount(tenantId, billDTO);
// Bill number from DTO or from auto-increment.
const billNumber = billDTO.billNumber || oldBill?.billNumber;
const initialEntries = billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount']),
}));
const entries = await composeAsync(
// Sets the default cost account to the bill entries.
this.setBillEntriesDefaultAccounts(tenantId)
)(initialEntries);
const initialDTO = {
...formatDateFields(omit(billDTO, ['open', 'entries']), [
'billDate',
'dueDate',
]),
amount,
landedCostAmount,
currencyCode: vendor.currencyCode,
exchangeRate: billDTO.exchangeRate || 1,
billNumber,
entries,
// Avoid rewrite the open date in edit mode when already opened.
...(billDTO.open &&
!oldBill?.openedAt && {
openedAt: moment().toMySqlDateTime(),
}),
userId: authorizedUser.id,
};
return R.compose(
this.branchDTOTransform.transformDTO(tenantId),
this.warehouseDTOTransform.transformDTO(tenantId)
)(initialDTO);
}
/**
* Creates a new bill and stored it to the storage.
* ----
* Precedures.
* ----
* - Insert bill transactions to the storage.
* - Insert bill entries to the storage.
* - Increment the given vendor id.
* - Record bill journal transactions on the given accounts.
* - Record bill items inventory transactions.
* ----
* @param {number} tenantId - The given tenant id.
* @param {IBillDTO} billDTO -
* @return {Promise<IBill>}
*/
public async createBill(
tenantId: number,
billDTO: IBillDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill, Contact } = this.tenancy.models(tenantId);
// Retrieves the given bill vendor or throw not found error.
const vendor = await Contact.query()
.modify('vendor')
.findById(billDTO.vendorId)
.throwIfNotFound();
// Validate the bill number uniqiness on the storage.
await this.validateBillNumberExists(tenantId, billDTO.billNumber);
// Validate items IDs existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
billDTO.entries
);
// Validate non-purchasable items.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(
tenantId,
billDTO.entries
);
// Validates the cost entries should be with inventory items.
await this.validateCostEntriesShouldBeInventoryItems(
tenantId,
billDTO.entries
);
// Transform the bill DTO to model object.
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
vendor,
authorizedUser
);
// Write new bill transaction with associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillCreating` event.
await this.eventPublisher.emitAsync(events.bill.onCreating, {
trx,
billDTO,
tenantId,
} as IBillCreatingPayload);
// Inserts the bill graph object to the storage.
const bill = await Bill.query(trx).upsertGraph(billObj);
// Triggers `onBillCreated` event.
await this.eventPublisher.emitAsync(events.bill.onCreated, {
tenantId,
bill,
billId: bill.id,
trx,
} as IBillCreatedPayload);
return bill;
});
}
/**
* Edits details of the given bill id with associated entries.
*
* Precedures:
* -------
* - Update the bill transaction on the storage.
* - Update the bill entries on the storage and insert the not have id and delete
* once that not presented.
* - Increment the diff amount on the given vendor id.
* - Re-write the inventory transactions.
* - Re-write the bill journal transactions.
* ------
* @param {number} tenantId - The given tenant id.
* @param {Integer} billId - The given bill id.
* @param {IBillEditDTO} billDTO - The given new bill details.
* @return {Promise<IBill>}
*/
public async editBill(
tenantId: number,
billId: number,
billDTO: IBillEditDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill, Contact } = this.tenancy.models(tenantId);
const oldBill = await this.getBillOrThrowError(tenantId, billId);
// Retrieve vendor details or throw not found service error.
const vendor = await Contact.query()
.findById(billDTO.vendorId)
.modify('vendor')
.throwIfNotFound();
// Validate bill number uniqiness on the storage.
if (billDTO.billNumber) {
await this.validateBillNumberExists(tenantId, billDTO.billNumber, billId);
}
// Validate the entries ids existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
billId,
'Bill',
billDTO.entries
);
// Validate the items ids existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
billDTO.entries
);
// Accept the purchasable items only.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(
tenantId,
billDTO.entries
);
// Transforms the bill DTO to model object.
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
vendor,
authorizedUser,
oldBill
);
// Validate landed cost entries that have allocated cost could not be deleted.
await this.entriesService.validateLandedCostEntriesNotDeleted(
oldBill.entries,
billObj.entries
);
// Validate new landed cost entries should be bigger than new entries.
await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries(
oldBill.entries,
billObj.entries
);
// Edits bill transactions and associated transactions under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillEditing` event.
await this.eventPublisher.emitAsync(events.bill.onEditing, {
trx,
tenantId,
oldBill,
billDTO,
} as IBillEditingPayload);
// Update the bill transaction.
const bill = await Bill.query(trx).upsertGraph({
id: billId,
...billObj,
});
// Triggers event `onBillEdited`.
await this.eventPublisher.emitAsync(events.bill.onEdited, {
tenantId,
billId,
oldBill,
bill,
trx,
} as IBillEditedPayload);
return bill;
});
}
/**
* Deletes the bill with associated entries.
* @param {Integer} billId
* @return {void}
*/
public async deleteBill(tenantId: number, billId: number) {
const { ItemEntry, Bill } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await this.getBillOrThrowError(tenantId, billId);
// Validate the givne bill has no associated landed cost transactions.
await this.validateBillHasNoLandedCost(tenantId, billId);
// Validate the purchase bill has no associated payments transactions.
await this.validateBillHasNoEntries(tenantId, billId);
// Validate the given bill has no associated reconciled with vendor credits.
await this.validateBillHasNoAppliedToCredit(tenantId, billId);
// Deletes bill transaction with associated transactions under
// unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillDeleting` event.
await this.eventPublisher.emitAsync(events.bill.onDeleting, {
trx,
tenantId,
oldBill,
} as IBillEventDeletingPayload);
// Delete all associated bill entries.
await ItemEntry.query(trx)
.where('reference_type', 'Bill')
.where('reference_id', billId)
.delete();
// Delete the bill transaction.
await Bill.query(trx).findById(billId).delete();
// Triggers `onBillDeleted` event.
await this.eventPublisher.emitAsync(events.bill.onDeleted, {
tenantId,
billId,
oldBill,
trx,
} as IBIllEventDeletedPayload);
});
}
validateBillHasNoAppliedToCredit = async (
tenantId: number,
billId: number
) => {
const { VendorCreditAppliedBill } = this.tenancy.models(tenantId);
const appliedTransactions = await VendorCreditAppliedBill.query().where(
'billId',
billId
);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.BILL_HAS_APPLIED_TO_VENDOR_CREDIT);
}
};
/**
* Parses bills list filter DTO.
* @param filterDTO -
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve bills data table list.
* @param {number} tenantId -
* @param {IBillsFilter} billsFilter -
*/
public async getBills(
tenantId: number,
filterDTO: IBillsFilter
): Promise<{
bills: IBill;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { Bill } = this.tenancy.models(tenantId);
// Parses bills list filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
Bill,
filter
);
const { results, pagination } = await Bill.query()
.onBuild((builder) => {
builder.withGraphFetched('vendor');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Tranform the bills to POJO.
const bills = await this.transformer.transform(
tenantId,
results,
new PurchaseInvoiceTransformer()
);
return {
bills,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Retrieve all due bills or for specific given vendor id.
* @param {number} tenantId -
* @param {number} vendorId -
*/
public async getDueBills(
tenantId: number,
vendorId?: number
): Promise<IBill[]> {
const { Bill } = this.tenancy.models(tenantId);
const dueBills = await Bill.query().onBuild((query) => {
query.orderBy('bill_date', 'DESC');
query.modify('dueBills');
if (vendorId) {
query.where('vendor_id', vendorId);
}
});
return dueBills;
}
/**
* Retrieve the given bill details with associated items entries.
* @param {Integer} billId - Specific bill.
* @returns {Promise<IBill>}
*/
public async getBill(tenantId: number, billId: number): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId);
const bill = await Bill.query()
.findById(billId)
.withGraphFetched('vendor')
.withGraphFetched('entries.item')
.withGraphFetched('branch');
if (!bill) {
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
}
return this.transformer.transform(
tenantId,
bill,
new PurchaseInvoiceTransformer()
);
}
/**
* Mark the bill as open.
* @param {number} tenantId
* @param {number} billId
*/
public async openBill(tenantId: number, billId: number): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await this.getBillOrThrowError(tenantId, billId);
if (oldBill.isOpen) {
throw new ServiceError(ERRORS.BILL_ALREADY_OPEN);
}
//
return this.uow.withTransaction(tenantId, async (trx) => {
// Record the bill opened at on the storage.
await Bill.query(trx).findById(billId).patch({
openedAt: moment().toMySqlDateTime(),
});
});
}
/**
* Records the inventory transactions from the given bill input.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async recordInventoryTransactions(
tenantId: number,
billId: number,
override?: boolean,
trx?: Knex.Transaction
): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
// Retireve bill with associated entries and allocated cost entries.
const bill = await Bill.query(trx)
.findById(billId)
.withGraphFetched('entries.allocatedCostEntries');
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
bill.entries
);
const transaction = {
transactionId: bill.id,
transactionType: 'Bill',
exchangeRate: bill.exchangeRate,
date: bill.billDate,
direction: 'IN',
entries: inventoryEntries,
createdAt: bill.createdAt,
warehouseId: bill.warehouseId,
};
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId,
transaction,
override,
trx
);
}
/**
* Reverts the inventory transactions of the given bill id.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
tenantId: number,
billId: number,
trx?: Knex.Transaction
) {
// Deletes the inventory transactions by the given reference id and type.
await this.inventoryService.deleteInventoryTransactions(
tenantId,
billId,
'Bill',
trx
);
}
/**
* Validate the given vendor has no associated bills transactions.
* @param {number} tenantId
* @param {number} vendorId - Vendor id.
*/
public async validateVendorHasNoBills(tenantId: number, vendorId: number) {
const { Bill } = this.tenancy.models(tenantId);
const bills = await Bill.query().where('vendor_id', vendorId);
if (bills.length > 0) {
throw new ServiceError(ERRORS.VENDOR_HAS_BILLS);
}
}
}

View File

@@ -0,0 +1,130 @@
import { omit, sumBy } from 'lodash';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import composeAsync from 'async/compose';
import { formatDateFields } from 'utils';
import {
IBillDTO,
IBill,
ISystemUser,
IVendor,
IItemEntry,
} from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class BillDTOTransformer {
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieve the bill entries total.
* @param {IItemEntry[]} entries
* @returns {number}
*/
private getBillEntriesTotal(tenantId: number, entries: IItemEntry[]): number {
const { ItemEntry } = this.tenancy.models(tenantId);
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
}
/**
* Retrieve the bill landed cost amount.
* @param {IBillDTO} billDTO
* @returns {number}
*/
private getBillLandedCostAmount(tenantId: number, billDTO: IBillDTO): number {
const costEntries = billDTO.entries.filter((entry) => entry.landedCost);
return this.getBillEntriesTotal(tenantId, costEntries);
}
/**
* Converts create bill DTO to model.
* @param {number} tenantId
* @param {IBillDTO} billDTO
* @param {IBill} oldBill
* @returns {IBill}
*/
public async billDTOToModel(
tenantId: number,
billDTO: IBillDTO,
vendor: IVendor,
authorizedUser: ISystemUser,
oldBill?: IBill
) {
const { ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e));
// Retrieve the landed cost amount from landed cost entries.
const landedCostAmount = this.getBillLandedCostAmount(tenantId, billDTO);
// Bill number from DTO or from auto-increment.
const billNumber = billDTO.billNumber || oldBill?.billNumber;
const initialEntries = billDTO.entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount']),
}));
const entries = await composeAsync(
// Sets the default cost account to the bill entries.
this.setBillEntriesDefaultAccounts(tenantId)
)(initialEntries);
const initialDTO = {
...formatDateFields(omit(billDTO, ['open', 'entries']), [
'billDate',
'dueDate',
]),
amount,
landedCostAmount,
currencyCode: vendor.currencyCode,
exchangeRate: billDTO.exchangeRate || 1,
billNumber,
entries,
// Avoid rewrite the open date in edit mode when already opened.
...(billDTO.open &&
!oldBill?.openedAt && {
openedAt: moment().toMySqlDateTime(),
}),
userId: authorizedUser.id,
};
return R.compose(
this.branchDTOTransform.transformDTO(tenantId),
this.warehouseDTOTransform.transformDTO(tenantId)
)(initialDTO);
}
/**
* Sets the default cost account to the bill entries.
*/
private setBillEntriesDefaultAccounts(tenantId: number) {
return async (entries: IItemEntry[]) => {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
const items = await Item.query().whereIn('id', entriesItemsIds);
return entries.map((entry) => {
const item = items.find((i) => i.id === entry.itemId);
return {
...entry,
...(item.type !== 'inventory' && {
costAccountId: entry.costAccountId || item.costAccountId,
}),
};
});
};
}
}

View File

@@ -1,30 +1,30 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import BillsService from '@/services/Purchases/Bills';
import { import {
IBillCreatedPayload, IBillCreatedPayload,
IBillEditedPayload, IBillEditedPayload,
IBIllEventDeletedPayload, IBIllEventDeletedPayload,
IBillOpenedPayload,
} from '@/interfaces'; } from '@/interfaces';
import { BillGLEntries } from './BillGLEntries'; import { BillGLEntries } from './BillGLEntries';
@Service() @Service()
export class BillGLEntriesSubscriber { export class BillGLEntriesSubscriber {
@Inject() @Inject()
tenancy: TenancyService; private billGLEntries: BillGLEntries;
@Inject()
billGLEntries: BillGLEntries;
/** /**
* Attaches events with handles. * Attaches events with handles.
*/ */
attach(bus) { public attach(bus) {
bus.subscribe( bus.subscribe(
events.bill.onCreated, events.bill.onCreated,
this.handlerWriteJournalEntriesOnCreate this.handlerWriteJournalEntriesOnCreate
); );
bus.subscribe(
events.bill.onOpened,
this.handlerWriteJournalEntriesOnCreate
);
bus.subscribe( bus.subscribe(
events.bill.onEdited, events.bill.onEdited,
this.handleOverwriteJournalEntriesOnEdit this.handleOverwriteJournalEntriesOnEdit
@@ -38,10 +38,12 @@ export class BillGLEntriesSubscriber {
*/ */
private handlerWriteJournalEntriesOnCreate = async ({ private handlerWriteJournalEntriesOnCreate = async ({
tenantId, tenantId,
billId, bill,
trx, trx,
}: IBillCreatedPayload) => { }: IBillCreatedPayload | IBillOpenedPayload) => {
await this.billGLEntries.writeBillGLEntries(tenantId, billId, trx); if (!bill.openedAt) return null;
await this.billGLEntries.writeBillGLEntries(tenantId, bill.id, trx);
}; };
/** /**
@@ -51,8 +53,11 @@ export class BillGLEntriesSubscriber {
private handleOverwriteJournalEntriesOnEdit = async ({ private handleOverwriteJournalEntriesOnEdit = async ({
tenantId, tenantId,
billId, billId,
bill,
trx, trx,
}: IBillEditedPayload) => { }: IBillEditedPayload) => {
if (!bill.openedAt) return null;
await this.billGLEntries.rewriteBillGLEntries(tenantId, billId, trx); await this.billGLEntries.rewriteBillGLEntries(tenantId, billId, trx);
}; };

View File

@@ -0,0 +1,82 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import InventoryService from '@/services/Inventory/Inventory';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class BillInventoryTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private inventoryService: InventoryService;
/**
* Records the inventory transactions from the given bill input.
* @param {Bill} bill - Bill model object.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async recordInventoryTransactions(
tenantId: number,
billId: number,
override?: boolean,
trx?: Knex.Transaction
): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
// Retireve bill with assocaited entries and allocated cost entries.
const bill = await Bill.query(trx)
.findById(billId)
.withGraphFetched('entries.allocatedCostEntries');
// Loads the inventory items entries of the given sale invoice.
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
bill.entries
);
const transaction = {
transactionId: bill.id,
transactionType: 'Bill',
exchangeRate: bill.exchangeRate,
date: bill.billDate,
direction: 'IN',
entries: inventoryEntries,
createdAt: bill.createdAt,
warehouseId: bill.warehouseId,
};
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
tenantId,
transaction,
override,
trx
);
}
/**
* Reverts the inventory transactions of the given bill id.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<void>}
*/
public async revertInventoryTransactions(
tenantId: number,
billId: number,
trx?: Knex.Transaction
) {
// Deletes the inventory transactions by the given reference id and type.
await this.inventoryService.deleteInventoryTransactions(
tenantId,
billId,
'Bill',
trx
);
}
}

View File

@@ -11,7 +11,7 @@ export class BillPaymentsGLEntriesRewriteSubscriber {
/** /**
* Attaches events with handles. * Attaches events with handles.
*/ */
attach(bus) { public attach(bus) {
bus.subscribe( bus.subscribe(
events.bill.onEdited, events.bill.onEdited,
this.handlerRewritePaymentsGLOnBillEdited this.handlerRewritePaymentsGLOnBillEdited

View File

@@ -0,0 +1,147 @@
import { Inject, Service } from 'typedi';
import { CreateBill } from './CreateBill';
import { EditBill } from './EditBill';
import { GetBill } from './GetBill';
import { GetBills } from './GetBills';
import { DeleteBill } from './DeleteBill';
import {
IBill,
IBillDTO,
IBillEditDTO,
IBillsFilter,
IFilterMeta,
IPaginationMeta,
ISystemUser,
} from '@/interfaces';
import { GetDueBills } from './GetDueBills';
import { OpenBill } from './OpenBill';
import { GetBillPayments } from './GetBillPayments';
@Service()
export class BillsApplication {
@Inject()
private createBillService: CreateBill;
@Inject()
private editBillService: EditBill;
@Inject()
private getBillService: GetBill;
@Inject()
private getBillsService: GetBills;
@Inject()
private deleteBillService: DeleteBill;
@Inject()
private getDueBillsService: GetDueBills;
@Inject()
private openBillService: OpenBill;
@Inject()
private getBillPaymentsService: GetBillPayments;
/**
* Creates a new bill with associated GL entries.
* @param {number} tenantId
* @param {IBillDTO} billDTO
* @param {ISystemUser} authorizedUser
* @returns
*/
public createBill(
tenantId: number,
billDTO: IBillDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
return this.createBillService.createBill(tenantId, billDTO, authorizedUser);
}
/**
* Edits the given bill with associated GL entries.
* @param {number} tenantId
* @param {number} billId
* @param {IBillEditDTO} billDTO
* @param {ISystemUser} authorizedUser
* @returns
*/
public editBill(
tenantId: number,
billId: number,
billDTO: IBillEditDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
return this.editBillService.editBill(
tenantId,
billId,
billDTO,
authorizedUser
);
}
/**
* Deletes the given bill with associated GL entries.
* @param {number} tenantId
* @param {number} billId
* @returns {Promise<void>}
*/
public deleteBill(tenantId: number, billId: number) {
return this.deleteBillService.deleteBill(tenantId, billId);
}
/**
* Retrieve bills data table list.
* @param {number} tenantId -
* @param {IBillsFilter} billsFilter -
*/
public getBills(
tenantId: number,
filterDTO: IBillsFilter
): Promise<{
bills: IBill;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
return this.getBillsService.getBills(tenantId, filterDTO);
}
/**
* Retrieves the given bill details.
* @param {number} tenantId
* @param {number} billId
* @returns
*/
public getBill(tenantId: number, billId: number): Promise<IBill> {
return this.getBillService.getBill(tenantId, billId);
}
/**
* Open the given bill.
* @param {number} tenantId
* @param {number} billId
* @returns {Promise<void>}
*/
public openBill(tenantId: number, billId: number): Promise<void> {
return this.openBillService.openBill(tenantId, billId);
}
/**
* Retrieves due bills of the given vendor.
* @param {number} tenantId
* @param {number} vendorId
* @returns
*/
public getDueBills(tenantId: number, vendorId?: number) {
return this.getDueBillsService.getDueBills(tenantId, vendorId);
}
/**
* Retrieve the specific bill associated payment transactions.
* @param {number} tenantId
* @param {number} billId
*/
public getBillPayments = async (tenantId: number, billId: number) => {
return this.getBillPaymentsService.getBillPayments(tenantId, billId);
};
}

View File

@@ -0,0 +1,154 @@
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ERRORS } from './constants';
import { IItemEntryDTO } from '@/interfaces';
import { transformToMap } from '@/utils';
import { Bill } from '@/models';
@Service()
export class BillsValidators {
@Inject()
private tenancy: HasTenancyService;
/**
* Validates the bill existance.
* @param {Bill | undefined | null} bill
*/
public validateBillExistance(bill: Bill | undefined | null) {
if (!bill) {
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
}
}
/**
* Validates the bill number existance.
*/
public async validateBillNumberExists(
tenantId: number,
billNumber: string,
notBillId?: number
) {
const { Bill } = this.tenancy.models(tenantId);
const foundBills = await Bill.query()
.where('bill_number', billNumber)
.onBuild((builder) => {
if (notBillId) {
builder.whereNot('id', notBillId);
}
});
if (foundBills.length > 0) {
throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS);
}
}
/**
* Validate the bill has no payment entries.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
*/
public async validateBillHasNoEntries(tenantId, billId: number) {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
// Retireve the bill associate payment made entries.
const entries = await BillPaymentEntry.query().where('bill_id', billId);
if (entries.length > 0) {
throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES);
}
return entries;
}
/**
* Validate the bill number require.
* @param {string} billNo -
*/
public validateBillNoRequire(billNo: string) {
if (!billNo) {
throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED);
}
}
/**
* Validate bill transaction has no associated allocated landed cost transactions.
* @param {number} tenantId
* @param {number} billId
*/
public async validateBillHasNoLandedCost(tenantId: number, billId: number) {
const { BillLandedCost } = this.tenancy.models(tenantId);
const billLandedCosts = await BillLandedCost.query().where(
'billId',
billId
);
if (billLandedCosts.length > 0) {
throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_LANDED_COSTS);
}
}
/**
* Validate transaction entries that have landed cost type should not be
* inventory items.
* @param {number} tenantId -
* @param {IItemEntryDTO[]} newEntriesDTO -
*/
public async validateCostEntriesShouldBeInventoryItems(
tenantId: number,
newEntriesDTO: IItemEntryDTO[]
) {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = newEntriesDTO.map((e) => e.itemId);
const entriesItems = await Item.query().whereIn('id', entriesItemsIds);
const entriesItemsById = transformToMap(entriesItems, 'id');
// Filter the landed cost entries that not associated with inventory item.
const nonInventoryHasCost = newEntriesDTO.filter((entry) => {
const item = entriesItemsById.get(entry.itemId);
return entry.landedCost && item.type !== 'inventory';
});
if (nonInventoryHasCost.length > 0) {
throw new ServiceError(
ERRORS.LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS
);
}
}
/**
*
* @param {number} tenantId
* @param {number} billId
*/
public validateBillHasNoAppliedToCredit = async (
tenantId: number,
billId: number
) => {
const { VendorCreditAppliedBill } = this.tenancy.models(tenantId);
const appliedTransactions = await VendorCreditAppliedBill.query().where(
'billId',
billId
);
if (appliedTransactions.length > 0) {
throw new ServiceError(ERRORS.BILL_HAS_APPLIED_TO_VENDOR_CREDIT);
}
};
/**
* Validate the given vendor has no associated bills transactions.
* @param {number} tenantId
* @param {number} vendorId - Vendor id.
*/
public async validateVendorHasNoBills(tenantId: number, vendorId: number) {
const { Bill } = this.tenancy.models(tenantId);
const bills = await Bill.query().where('vendor_id', vendorId);
if (bills.length > 0) {
throw new ServiceError(ERRORS.VENDOR_HAS_BILLS);
}
}
}

View File

@@ -0,0 +1,116 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import {
IBillDTO,
IBill,
ISystemUser,
IBillCreatedPayload,
IBillCreatingPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import { BillsValidators } from './BillsValidators';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import { BillDTOTransformer } from './BillDTOTransformer';
@Service()
export class CreateBill {
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private validators: BillsValidators;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private transformerDTO: BillDTOTransformer;
/**
* Creates a new bill and stored it to the storage.
* ----
* Precedures.
* ----
* - Insert bill transactions to the storage.
* - Insert bill entries to the storage.
* - Increment the given vendor id.
* - Record bill journal transactions on the given accounts.
* - Record bill items inventory transactions.
* ----
* @param {number} tenantId - The given tenant id.
* @param {IBillDTO} billDTO -
* @return {Promise<IBill>}
*/
public async createBill(
tenantId: number,
billDTO: IBillDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill, Contact } = this.tenancy.models(tenantId);
// Retrieves the given bill vendor or throw not found error.
const vendor = await Contact.query()
.modify('vendor')
.findById(billDTO.vendorId)
.throwIfNotFound();
// Validate the bill number uniqiness on the storage.
await this.validators.validateBillNumberExists(
tenantId,
billDTO.billNumber
);
// Validate items IDs existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
billDTO.entries
);
// Validate non-purchasable items.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(
tenantId,
billDTO.entries
);
// Validates the cost entries should be with inventory items.
await this.validators.validateCostEntriesShouldBeInventoryItems(
tenantId,
billDTO.entries
);
// Transform the bill DTO to model object.
const billObj = await this.transformerDTO.billDTOToModel(
tenantId,
billDTO,
vendor,
authorizedUser
);
// Write new bill transaction with associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillCreating` event.
await this.eventPublisher.emitAsync(events.bill.onCreating, {
trx,
billDTO,
tenantId,
} as IBillCreatingPayload);
// Inserts the bill graph object to the storage.
const bill = await Bill.query(trx).upsertGraph(billObj);
// Triggers `onBillCreated` event.
await this.eventPublisher.emitAsync(events.bill.onCreated, {
tenantId,
bill,
billId: bill.id,
trx,
} as IBillCreatedPayload);
return bill;
});
}
}

View File

@@ -0,0 +1,80 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import {
IBIllEventDeletedPayload,
IBillEventDeletingPayload,
} from '@/interfaces';
import { BillsValidators } from './BillsValidators';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class DeleteBill {
@Inject()
private validators: BillsValidators;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
/**
* Deletes the bill with associated entries.
* @param {number} billId
* @return {void}
*/
public async deleteBill(tenantId: number, billId: number) {
const { ItemEntry, Bill } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await Bill.query()
.findById(billId)
.withGraphFetched('entries');
// Validates the bill existance.
this.validators.validateBillExistance(oldBill);
// Validate the givne bill has no associated landed cost transactions.
await this.validators.validateBillHasNoLandedCost(tenantId, billId);
// Validate the purchase bill has no assocaited payments transactions.
await this.validators.validateBillHasNoEntries(tenantId, billId);
// Validate the given bill has no associated reconciled with vendor credits.
await this.validators.validateBillHasNoAppliedToCredit(tenantId, billId);
// Deletes bill transaction with associated transactions under
// unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillDeleting` event.
await this.eventPublisher.emitAsync(events.bill.onDeleting, {
trx,
tenantId,
oldBill,
} as IBillEventDeletingPayload);
// Delete all associated bill entries.
await ItemEntry.query(trx)
.where('reference_type', 'Bill')
.where('reference_id', billId)
.delete();
// Delete the bill transaction.
await Bill.query(trx).findById(billId).delete();
// Triggers `onBillDeleted` event.
await this.eventPublisher.emitAsync(events.bill.onDeleted, {
tenantId,
billId,
oldBill,
trx,
} as IBIllEventDeletedPayload);
});
}
}

View File

@@ -0,0 +1,151 @@
import {
IBill,
IBillEditDTO,
IBillEditedPayload,
IBillEditingPayload,
ISystemUser,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { BillsValidators } from './BillsValidators';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import UnitOfWork from '@/services/UnitOfWork';
import { Knex } from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import EntriesService from '@/services/Entries';
import { BillDTOTransformer } from './BillDTOTransformer';
@Service()
export class EditBill {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private validators: BillsValidators;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private entriesService: EntriesService;
@Inject()
private transformerDTO: BillDTOTransformer;
/**
* Edits details of the given bill id with associated entries.
*
* Precedures:
* -------
* - Update the bill transaction on the storage.
* - Update the bill entries on the storage and insert the not have id and delete
* once that not presented.
* - Increment the diff amount on the given vendor id.
* - Re-write the inventory transactions.
* - Re-write the bill journal transactions.
* ------
* @param {number} tenantId - The given tenant id.
* @param {Integer} billId - The given bill id.
* @param {IBillEditDTO} billDTO - The given new bill details.
* @return {Promise<IBill>}
*/
public async editBill(
tenantId: number,
billId: number,
billDTO: IBillEditDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill, Contact } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await Bill.query()
.findById(billId)
.withGraphFetched('entries');
// Validate bill existance.
this.validators.validateBillExistance(oldBill);
// Retrieve vendor details or throw not found service error.
const vendor = await Contact.query()
.findById(billDTO.vendorId)
.modify('vendor')
.throwIfNotFound();
// Validate bill number uniqiness on the storage.
if (billDTO.billNumber) {
await this.validators.validateBillNumberExists(
tenantId,
billDTO.billNumber,
billId
);
}
// Validate the entries ids existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
billId,
'Bill',
billDTO.entries
);
// Validate the items ids existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
billDTO.entries
);
// Accept the purchasable items only.
await this.itemsEntriesService.validateNonPurchasableEntriesItems(
tenantId,
billDTO.entries
);
// Transforms the bill DTO to model object.
const billObj = await this.transformerDTO.billDTOToModel(
tenantId,
billDTO,
vendor,
authorizedUser,
oldBill
);
// Validate landed cost entries that have allocated cost could not be deleted.
await this.entriesService.validateLandedCostEntriesNotDeleted(
oldBill.entries,
billObj.entries
);
// Validate new landed cost entries should be bigger than new entries.
await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries(
oldBill.entries,
billObj.entries
);
// Edits bill transactions and associated transactions under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillEditing` event.
await this.eventPublisher.emitAsync(events.bill.onEditing, {
trx,
tenantId,
oldBill,
billDTO,
} as IBillEditingPayload);
// Update the bill transaction.
const bill = await Bill.query(trx).upsertGraphAndFetch({
id: billId,
...billObj,
});
// Triggers event `onBillEdited`.
await this.eventPublisher.emitAsync(events.bill.onEdited, {
tenantId,
billId,
oldBill,
bill,
trx,
} as IBillEditedPayload);
return bill;
});
}
}

View File

@@ -0,0 +1,42 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { IBill } from '@/interfaces';
import { BillsValidators } from './BillsValidators';
import { PurchaseInvoiceTransformer } from './PurchaseInvoiceTransformer';
@Service()
export class GetBill {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private validators: BillsValidators;
/**
* Retrieve the given bill details with associated items entries.
* @param {Integer} billId - Specific bill.
* @returns {Promise<IBill>}
*/
public async getBill(tenantId: number, billId: number): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId);
const bill = await Bill.query()
.findById(billId)
.withGraphFetched('vendor')
.withGraphFetched('entries.item')
.withGraphFetched('branch');
// Validates the bill existance.
this.validators.validateBillExistance(bill);
return this.transformer.transform(
tenantId,
bill,
new PurchaseInvoiceTransformer()
);
}
}

View File

@@ -1,10 +1,10 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import { BillPaymentTransactionTransformer } from './BillPayments/BillPaymentTransactionTransformer'; import { BillPaymentTransactionTransformer } from '../BillPayments/BillPaymentTransactionTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service() @Service()
export default class BillPaymentsService { export class GetBillPayments {
@Inject() @Inject()
private tenancy: TenancyService; private tenancy: TenancyService;

View File

@@ -0,0 +1,76 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import {
IBill,
IBillsFilter,
IFilterMeta,
IPaginationMeta,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PurchaseInvoiceTransformer } from './PurchaseInvoiceTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
@Service()
export class GetBills {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private dynamicListService: DynamicListingService;
/**
* Retrieve bills data table list.
* @param {number} tenantId -
* @param {IBillsFilter} billsFilter -
*/
public async getBills(
tenantId: number,
filterDTO: IBillsFilter
): Promise<{
bills: IBill;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { Bill } = this.tenancy.models(tenantId);
// Parses bills list filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
Bill,
filter
);
const { results, pagination } = await Bill.query()
.onBuild((builder) => {
builder.withGraphFetched('vendor');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Tranform the bills to POJO.
const bills = await this.transformer.transform(
tenantId,
results,
new PurchaseInvoiceTransformer()
);
return {
bills,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Parses bills list filter DTO.
* @param filterDTO -
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,32 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { IBill } from '@/interfaces';
@Service()
export class GetDueBills {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieve all due bills or for specific given vendor id.
* @param {number} tenantId -
* @param {number} vendorId -
*/
public async getDueBills(
tenantId: number,
vendorId?: number
): Promise<IBill[]> {
const { Bill } = this.tenancy.models(tenantId);
const dueBills = await Bill.query().onBuild((query) => {
query.orderBy('bill_date', 'DESC');
query.modify('dueBills');
if (vendorId) {
query.where('vendor_id', vendorId);
}
});
return dueBills;
}
}

View File

@@ -0,0 +1,69 @@
import moment from 'moment';
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { BillsValidators } from './BillsValidators';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { IBillOpenedPayload, IBillOpeningPayload } from '@/interfaces';
@Service()
export class OpenBill {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private validators: BillsValidators;
@Inject()
private eventPublisher: EventPublisher;
/**
* Mark the bill as open.
* @param {number} tenantId
* @param {number} billId
*/
public async openBill(tenantId: number, billId: number): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
// Retrieve the given bill or throw not found error.
const oldBill = await Bill.query()
.findById(billId)
.withGraphFetched('entries');
// Validates the bill existance.
this.validators.validateBillExistance(oldBill);
if (oldBill.isOpen) {
throw new ServiceError(ERRORS.BILL_ALREADY_OPEN);
}
return this.uow.withTransaction(tenantId, async (trx) => {
// Triggers `onBillCreating` event.
await this.eventPublisher.emitAsync(events.bill.onOpening, {
trx,
tenantId,
oldBill,
} as IBillOpeningPayload);
// Save the bill opened at on the storage.
const bill = await Bill.query(trx)
.patchAndFetchById(billId, {
openedAt: moment().toMySqlDateTime(),
})
.withGraphFetched('entries');
// Triggers `onBillCreating` event.
await this.eventPublisher.emitAsync(events.bill.onOpened, {
trx,
bill,
oldBill,
tenantId,
} as IBillOpenedPayload);
});
}
}

View File

@@ -41,13 +41,16 @@ export default class AllocateLandedCost extends BaseLandedCostService {
allocateCostDTO: ILandedCostDTO, allocateCostDTO: ILandedCostDTO,
billId: number billId: number
): Promise<IBillLandedCost> => { ): Promise<IBillLandedCost> => {
const { BillLandedCost } = this.tenancy.models(tenantId); const { BillLandedCost, Bill } = this.tenancy.models(tenantId);
// Retrieve total cost of allocated items. // Retrieve total cost of allocated items.
const amount = this.getAllocateItemsCostTotal(allocateCostDTO); const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
// Retrieve the purchase invoice or throw not found error. // Retrieve the purchase invoice or throw not found error.
const bill = await this.billsService.getBillOrThrowError(tenantId, billId); const bill = await Bill.query()
.findById(billId)
.withGraphFetched('entries')
.throwIfNotFound();
// Retrieve landed cost transaction or throw not found service error. // Retrieve landed cost transaction or throw not found service error.
const costTransaction = await this.getLandedCostOrThrowError( const costTransaction = await this.getLandedCostOrThrowError(

View File

@@ -1,6 +1,5 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { difference, sumBy } from 'lodash'; import { difference, sumBy } from 'lodash';
import BillsService from '../Bills';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
IItemEntry, IItemEntry,
@@ -13,14 +12,10 @@ import {
} from '@/interfaces'; } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import TransactionLandedCost from './TransctionLandedCost'; import TransactionLandedCost from './TransctionLandedCost';
import { ERRORS } from './utils'; import { ERRORS, CONFIG } from './utils';
import { CONFIG } from './utils';
@Service() @Service()
export default class BaseLandedCostService { export default class BaseLandedCostService {
@Inject()
public billsService: BillsService;
@Inject() @Inject()
public tenancy: HasTenancyService; public tenancy: HasTenancyService;

View File

@@ -9,7 +9,7 @@ import LandedCostSyncCostTransactions from './LandedCostSyncCostTransactions';
@Service() @Service()
export default class LandedCostSyncCostTransactionsSubscriber { export default class LandedCostSyncCostTransactionsSubscriber {
@Inject() @Inject()
landedCostSyncCostTransaction: LandedCostSyncCostTransactions; private landedCostSyncCostTransaction: LandedCostSyncCostTransactions;
/** /**
* Attaches events with handlers. * Attaches events with handlers.

View File

@@ -9,20 +9,12 @@ import {
ILandedCostTransactionEntryDOJO, ILandedCostTransactionEntryDOJO,
} from '@/interfaces'; } from '@/interfaces';
import TransactionLandedCost from './TransctionLandedCost'; import TransactionLandedCost from './TransctionLandedCost';
import BillsService from '../Bills';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { formatNumber } from 'utils'; import { formatNumber } from 'utils';
@Service() @Service()
export default class LandedCostTranasctions { export default class LandedCostTranasctions {
@Inject() @Inject()
transactionLandedCost: TransactionLandedCost; private transactionLandedCost: TransactionLandedCost;
@Inject()
billsService: BillsService;
@Inject()
tenancy: HasTenancyService;
/** /**
* Retrieve the landed costs based on the given query. * Retrieve the landed costs based on the given query.

View File

@@ -16,13 +16,13 @@ import { ERRORS } from './utils';
@Service() @Service()
export default class TransactionLandedCost { export default class TransactionLandedCost {
@Inject() @Inject()
billLandedCost: BillLandedCost; private billLandedCost: BillLandedCost;
@Inject() @Inject()
expenseLandedCost: ExpenseLandedCost; private expenseLandedCost: ExpenseLandedCost;
@Inject() @Inject()
tenancy: HasTenancyService; private tenancy: HasTenancyService;
/** /**
* Retrieve the cost transaction code model. * Retrieve the cost transaction code model.

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 {
IVendorCredit, IVendorCredit,
@@ -9,27 +9,27 @@ import {
IBill, IBill,
} 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 events from '@/subscribers/events'; import events from '@/subscribers/events';
import VendorCredit from '../BaseVendorCredit'; import VendorCredit from '../BaseVendorCredit';
import BillPaymentsService from '@/services/Purchases/BillPayments/BillPayments';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { BillPaymentValidators } from '../../BillPayments/BillPaymentValidators';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from '../constants'; import { ERRORS } from '../constants';
@Service() @Service()
export default class ApplyVendorCreditToBills extends VendorCredit { export default class ApplyVendorCreditToBills extends VendorCredit {
@Inject('PaymentReceives') @Inject()
paymentReceive: PaymentReceiveService; private tenancy: HasTenancyService;
@Inject() @Inject()
uow: UnitOfWork; private uow: UnitOfWork;
@Inject() @Inject()
eventPublisher: EventPublisher; private eventPublisher: EventPublisher;
@Inject() @Inject()
billPayment: BillPaymentsService; private billPaymentValidators: BillPaymentValidators;
/** /**
* Apply credit note to the given invoices. * Apply credit note to the given invoices.
@@ -55,11 +55,12 @@ export default class ApplyVendorCreditToBills extends VendorCredit {
vendorCredit vendorCredit
); );
// Validate bills entries existance. // Validate bills entries existance.
const appliedBills = await this.billPayment.validateBillsExistance( const appliedBills =
tenantId, await this.billPaymentValidators.validateBillsExistance(
vendorCreditAppliedModel.entries, tenantId,
vendorCredit.vendorId vendorCreditAppliedModel.entries,
); vendorCredit.vendorId
);
// Validate bills has remaining amount to apply. // Validate bills has remaining amount to apply.
this.validateBillsRemainingAmount( this.validateBillsRemainingAmount(
appliedBills, appliedBills,

View File

@@ -10,7 +10,7 @@ import VendorCreditInventoryTransactions from './VendorCreditInventoryTransactio
@Service() @Service()
export default class VendorCreditInventoryTransactionsSubscriber { export default class VendorCreditInventoryTransactionsSubscriber {
@Inject() @Inject()
inventoryTransactions: VendorCreditInventoryTransactions; private inventoryTransactions: VendorCreditInventoryTransactions;
/** /**
* Attaches events with handlers. * Attaches events with handlers.
@@ -21,6 +21,10 @@ export default class VendorCreditInventoryTransactionsSubscriber {
events.vendorCredit.onCreated, events.vendorCredit.onCreated,
this.writeInventoryTransactionsOnceCreated this.writeInventoryTransactionsOnceCreated
); );
bus.subscribe(
events.vendorCredit.onOpened,
this.writeInventoryTransactionsOnceCreated
);
bus.subscribe( bus.subscribe(
events.vendorCredit.onEdited, events.vendorCredit.onEdited,
this.rewriteInventroyTransactionsOnceEdited this.rewriteInventroyTransactionsOnceEdited
@@ -40,6 +44,9 @@ export default class VendorCreditInventoryTransactionsSubscriber {
vendorCredit, vendorCredit,
trx, trx,
}: IVendorCreditCreatedPayload) => { }: IVendorCreditCreatedPayload) => {
// Can't continue if vendor credit is not opened.
if (!vendorCredit.openedAt) return null;
await this.inventoryTransactions.createInventoryTransactions( await this.inventoryTransactions.createInventoryTransactions(
tenantId, tenantId,
vendorCredit, vendorCredit,
@@ -57,6 +64,9 @@ export default class VendorCreditInventoryTransactionsSubscriber {
vendorCredit, vendorCredit,
trx, trx,
}: IVendorCreditEditedPayload) => { }: IVendorCreditEditedPayload) => {
// Can't continue if vendor credit is not opened.
if (!vendorCredit.openedAt) return null;
await this.inventoryTransactions.editInventoryTransactions( await this.inventoryTransactions.editInventoryTransactions(
tenantId, tenantId,
vendorCreditId, vendorCreditId,

View File

@@ -0,0 +1,75 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import {
ISaleEstimateApprovedEvent,
ISaleEstimateApprovingEvent,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { ERRORS } from './constants';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import moment from 'moment';
@Service()
export class ApproveSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Mark the sale estimate as approved from the customer.
* @param {number} tenantId
* @param {number} saleEstimateId
*/
public async approveSaleEstimate(
tenantId: number,
saleEstimateId: number
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const oldSaleEstimate = await SaleEstimate.query()
.findById(saleEstimateId)
.throwIfNotFound();
// Throws error in case the sale estimate still not delivered to customer.
if (!oldSaleEstimate.isDelivered) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
}
// Throws error in case the sale estimate already approved.
if (oldSaleEstimate.isApproved) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_APPROVED);
}
// Triggers `onSaleEstimateApproving` event.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimateApproving` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onApproving, {
trx,
tenantId,
oldSaleEstimate,
} as ISaleEstimateApprovingEvent);
// Update estimate as approved.
const saleEstimate = await SaleEstimate.query(trx)
.where('id', saleEstimateId)
.patch({
approvedAt: moment().toMySqlDateTime(),
rejectedAt: null,
});
// Triggers `onSaleEstimateApproved` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onApproved, {
trx,
tenantId,
oldSaleEstimate,
saleEstimate,
} as ISaleEstimateApprovedEvent);
});
}
}

View File

@@ -0,0 +1,46 @@
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import { Knex } from 'knex';
import moment from 'moment';
import { Inject, Service } from 'typedi';
@Service()
export class ConvertSaleEstimate {
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private tenancy: HasTenancyService;
/**
* Converts estimate to invoice.
* @param {number} tenantId -
* @param {number} estimateId -
* @return {Promise<void>}
*/
public async convertEstimateToInvoice(
tenantId: number,
estimateId: number,
invoiceId: number,
trx?: Knex.Transaction
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate.
const saleEstimate = await SaleEstimate.query()
.findById(estimateId)
.throwIfNotFound();
// Marks the estimate as converted from the givne invoice.
await SaleEstimate.query(trx).where('id', estimateId).patch({
convertedToInvoiceId: invoiceId,
convertedToInvoiceAt: moment().toMySqlDateTime(),
});
// Triggers `onSaleEstimateConvertedToInvoice` event.
await this.eventPublisher.emitAsync(
events.saleEstimate.onConvertedToInvoice,
{}
);
}
}

View File

@@ -0,0 +1,102 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import {
ISaleEstimate,
ISaleEstimateCreatedPayload,
ISaleEstimateCreatingPayload,
ISaleEstimateDTO,
} from '@/interfaces';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer';
import events from '@/subscribers/events';
import { SaleEstimateValidators } from './SaleEstimateValidators';
@Service()
export class CreateSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private transformerDTO: SaleEstimateDTOTransformer;
@Inject()
private validators: SaleEstimateValidators;
/**
* Creates a new estimate with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {EstimateDTO} estimate
* @return {Promise<ISaleEstimate>}
*/
public async createEstimate(
tenantId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate, Contact } = this.tenancy.models(tenantId);
// Retrieve the given customer or throw not found service error.
const customer = await Contact.query()
.modify('customer')
.findById(estimateDTO.customerId)
.throwIfNotFound();
// Transform DTO object ot model object.
const estimateObj = await this.transformerDTO.transformDTOToModel(
tenantId,
estimateDTO,
customer
);
// Validate estimate number uniquiness on the storage.
await this.validators.validateEstimateNumberExistance(
tenantId,
estimateObj.estimateNumber
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
estimateDTO.entries
);
// Validate non-sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
estimateDTO.entries
);
// Creates a sale estimate transaction with associated transactions as UOW.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimateCreating` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, {
estimateDTO,
tenantId,
trx,
} as ISaleEstimateCreatingPayload);
// Upsert the sale estimate graph to the storage.
const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({
...estimateObj,
});
// Triggers `onSaleEstimateCreated` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, {
tenantId,
saleEstimate,
saleEstimateId: saleEstimate.id,
saleEstimateDTO: estimateDTO,
trx,
} as ISaleEstimateCreatedPayload);
return saleEstimate;
});
}
}

View File

@@ -0,0 +1,74 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import {
ISaleEstimateDeletedPayload,
ISaleEstimateDeletingPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { ERRORS } from './constants';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import { Knex } from 'knex';
@Service()
export class DeleteSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Deletes the given estimate id with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {IEstimate} estimateId
* @return {void}
*/
public async deleteEstimate(
tenantId: number,
estimateId: number
): Promise<void> {
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
// Retrieve sale estimate or throw not found service error.
const oldSaleEstimate = await SaleEstimate.query()
.findById(estimateId)
.throwIfNotFound();
// Throw error if the sale estimate converted to sale invoice.
if (oldSaleEstimate.convertedToInvoiceId) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
}
// Deletes the estimate with associated transactions under UOW enivrement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimatedDeleting` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onDeleting, {
trx,
tenantId,
oldSaleEstimate,
} as ISaleEstimateDeletingPayload);
// Delete sale estimate entries.
await ItemEntry.query(trx)
.where('reference_id', estimateId)
.where('reference_type', 'SaleEstimate')
.delete();
// Delete sale estimate transaction.
await SaleEstimate.query(trx).where('id', estimateId).delete();
// Triggers `onSaleEstimatedDeleted` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onDeleted, {
tenantId,
saleEstimateId: estimateId,
oldSaleEstimate,
trx,
} as ISaleEstimateDeletedPayload);
});
}
}

View File

@@ -0,0 +1,71 @@
import { ServiceError } from '@/exceptions';
import {
ISaleEstimateEventDeliveredPayload,
ISaleEstimateEventDeliveringPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import moment from 'moment';
@Service()
export class DeliverSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Mark the sale estimate as delivered.
* @param {number} tenantId - Tenant id.
* @param {number} saleEstimateId - Sale estimate id.
*/
public async deliverSaleEstimate(
tenantId: number,
saleEstimateId: number
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const oldSaleEstimate = await SaleEstimate.query()
.findById(saleEstimateId)
.throwIfNotFound();
// Throws error in case the sale estimate already published.
if (oldSaleEstimate.isDelivered) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED);
}
// Updates the sale estimate transaction with assocaited transactions
// under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimateDelivering` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onDelivering, {
oldSaleEstimate,
trx,
tenantId,
} as ISaleEstimateEventDeliveringPayload);
// Record the delivered at on the storage.
const saleEstimate = await SaleEstimate.query(trx).patchAndFetchById(
saleEstimateId,
{
deliveredAt: moment().toMySqlDateTime(),
}
);
// Triggers `onSaleEstimateDelivered` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onDelivered, {
tenantId,
saleEstimate,
trx,
} as ISaleEstimateEventDeliveredPayload);
});
}
}

View File

@@ -0,0 +1,123 @@
import { Inject, Service } from 'typedi';
import {
ISaleEstimate,
ISaleEstimateDTO,
ISaleEstimateEditedPayload,
ISaleEstimateEditingPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleEstimateValidators } from './SaleEstimateValidators';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import events from '@/subscribers/events';
@Service()
export class EditSaleEstimate {
@Inject()
private validators: SaleEstimateValidators;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private transformerDTO: SaleEstimateDTOTransformer;
/**
* Edit details of the given estimate with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {Integer} estimateId
* @param {EstimateDTO} estimate
* @return {Promise<ISaleEstimate>}
*/
public async editEstimate(
tenantId: number,
estimateId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate, Contact } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const oldSaleEstimate = await SaleEstimate.query().findById(estimateId);
// Validates the given estimate existance.
this.validators.validateEstimateExistance(oldSaleEstimate);
// Retrieve the given customer or throw not found service error.
const customer = await Contact.query()
.modify('customer')
.findById(estimateDTO.customerId)
.throwIfNotFound();
// Transform DTO object ot model object.
const estimateObj = await this.transformerDTO.transformDTOToModel(
tenantId,
estimateDTO,
oldSaleEstimate,
customer
);
// Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) {
await this.validators.validateEstimateNumberExistance(
tenantId,
estimateDTO.estimateNumber,
estimateId
);
}
// Validate sale estimate entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
estimateId,
'SaleEstimate',
estimateDTO.entries
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
estimateDTO.entries
);
// Validate non-sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
estimateDTO.entries
);
// Edits estimate transaction with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx) => {
// Trigger `onSaleEstimateEditing` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onEditing, {
tenantId,
oldSaleEstimate,
estimateDTO,
trx,
} as ISaleEstimateEditingPayload);
// Upsert the estimate graph to the storage.
const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({
id: estimateId,
...estimateObj,
});
// Trigger `onSaleEstimateEdited` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onEdited, {
tenantId,
estimateId,
saleEstimate,
oldSaleEstimate,
trx,
} as ISaleEstimateEditedPayload);
return saleEstimate;
});
}
}

View File

@@ -0,0 +1,43 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { SaleEstimateTransfromer } from './SaleEstimateTransformer';
import { SaleEstimateValidators } from './SaleEstimateValidators';
@Service()
export class GetSaleEstimate {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private validators: SaleEstimateValidators;
/**
* Retrieve the estimate details with associated entries.
* @async
* @param {number} tenantId - The tenant id.
* @param {Integer} estimateId
*/
public async getEstimate(tenantId: number, estimateId: number) {
const { SaleEstimate } = this.tenancy.models(tenantId);
const estimate = await SaleEstimate.query()
.findById(estimateId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('branch');
// Validates the estimate existance.
this.validators.validateEstimateExistance(estimate);
// Transformes sale estimate model to POJO.
return this.transformer.transform(
tenantId,
estimate,
new SaleEstimateTransfromer()
);
}
}

View File

@@ -0,0 +1,77 @@
import * as R from 'ramda';
import { Inject, Service } from 'typedi';
import {
IFilterMeta,
IPaginationMeta,
ISaleEstimate,
ISalesEstimatesFilter,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer';
import { SaleEstimateTransfromer } from './SaleEstimateTransformer';
@Service()
export class GetSaleEstimates {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves estimates filterable and paginated list.
* @param {number} tenantId -
* @param {IEstimatesFilter} estimatesFilter -
*/
public async getEstimates(
tenantId: number,
filterDTO: ISalesEstimatesFilter
): Promise<{
salesEstimates: ISaleEstimate[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
SaleEstimate,
filter
);
const { results, pagination } = await SaleEstimate.query()
.onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
const transformedEstimates = await this.transformer.transform(
tenantId,
results,
new SaleEstimateTransfromer()
);
return {
salesEstimates: transformedEstimates,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Parses the sale receipts list filter DTO.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,57 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service()
export class RejectSaleEstimate {
@Inject()
private tenancy: TenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Mark the sale estimate as rejected from the customer.
* @param {number} tenantId
* @param {number} saleEstimateId
*/
public async rejectSaleEstimate(
tenantId: number,
saleEstimateId: number
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const saleEstimate = await SaleEstimate.query()
.findById(saleEstimateId)
.throwIfNotFound();
// Throws error in case the sale estimate still not delivered to customer.
if (!saleEstimate.isDelivered) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
}
// Throws error in case the sale estimate already rejected.
if (saleEstimate.isRejected) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_REJECTED);
}
//
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Mark the sale estimate as reject on the storage.
await SaleEstimate.query(trx).where('id', saleEstimateId).patch({
rejectedAt: moment().toMySqlDateTime(),
approvedAt: null,
});
// Triggers `onSaleEstimateRejected` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onRejected, {});
});
}
}

View File

@@ -0,0 +1,104 @@
import * as R from 'ramda';
import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ICustomer, ISaleEstimate, ISaleEstimateDTO } from '@/interfaces';
import { SaleEstimateValidators } from './SaleEstimateValidators';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { formatDateFields } from '@/utils';
import moment from 'moment';
import { SaleEstimateIncrement } from './SaleEstimateIncrement';
@Service()
export class SaleEstimateDTOTransformer {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private validators: SaleEstimateValidators;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private estimateIncrement: SaleEstimateIncrement;
/**
* Transform create DTO object ot model object.
* @param {number} tenantId
* @param {ISaleEstimateDTO} saleEstimateDTO - Sale estimate DTO.
* @return {ISaleEstimate}
*/
async transformDTOToModel(
tenantId: number,
estimateDTO: ISaleEstimateDTO,
paymentCustomer: ICustomer,
oldSaleEstimate?: ISaleEstimate
): Promise<ISaleEstimate> {
const { ItemEntry, Contact } = this.tenancy.models(tenantId);
const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e));
// Retreive the next invoice number.
const autoNextNumber =
this.estimateIncrement.getNextEstimateNumber(tenantId);
// Retreive the next estimate number.
const estimateNumber =
estimateDTO.estimateNumber ||
oldSaleEstimate?.estimateNumber ||
autoNextNumber;
// Validate the sale estimate number require.
this.validators.validateEstimateNoRequire(estimateNumber);
const initialDTO = {
amount,
...formatDateFields(omit(estimateDTO, ['delivered', 'entries']), [
'estimateDate',
'expirationDate',
]),
currencyCode: paymentCustomer.currencyCode,
exchangeRate: estimateDTO.exchangeRate || 1,
...(estimateNumber ? { estimateNumber } : {}),
entries: estimateDTO.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...entry,
})),
// Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered &&
!oldSaleEstimate?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
};
return R.compose(
this.branchDTOTransform.transformDTO<ISaleEstimate>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleEstimate>(tenantId)
)(initialDTO);
}
/**
* Retrieve estimate number to object model.
* @param {number} tenantId
* @param {ISaleEstimateDTO} saleEstimateDTO
* @param {ISaleEstimate} oldSaleEstimate
*/
public transformEstimateNumberToModel(
tenantId: number,
saleEstimateDTO: ISaleEstimateDTO,
oldSaleEstimate?: ISaleEstimate
): string {
// Retreive the next invoice number.
const autoNextNumber =
this.estimateIncrement.getNextEstimateNumber(tenantId);
if (saleEstimateDTO.estimateNumber) {
return saleEstimateDTO.estimateNumber;
}
return oldSaleEstimate ? oldSaleEstimate.estimateNumber : autoNextNumber;
}
}

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import AutoIncrementOrdersService from '../AutoIncrementOrdersService';
@Service()
export class SaleEstimateIncrement {
@Inject()
private autoIncrementOrdersService: AutoIncrementOrdersService;
/**
* Retrieve the next unique estimate number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
public getNextEstimateNumber(tenantId: number): string {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'sales_estimates'
);
}
/**
* Increment the estimate next number.
* @param {number} tenantId -
*/
public incrementNextEstimateNumber(tenantId: number) {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'sales_estimates'
);
}
}

View File

@@ -4,7 +4,6 @@ import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import SaleNotifyBySms from '../SaleNotifyBySms'; import SaleNotifyBySms from '../SaleNotifyBySms';
import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings'; import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
import SMSClient from '@/services/SMSClient';
import { import {
ICustomer, ICustomer,
IPaymentReceiveSmsDetails, IPaymentReceiveSmsDetails,
@@ -21,18 +20,18 @@ const ERRORS = {
}; };
@Service() @Service()
export default class SaleEstimateNotifyBySms { export class SaleEstimateNotifyBySms {
@Inject() @Inject()
tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject() @Inject()
saleSmsNotification: SaleNotifyBySms; private saleSmsNotification: SaleNotifyBySms;
@Inject() @Inject()
eventPublisher: EventPublisher; private eventPublisher: EventPublisher;
@Inject() @Inject()
smsNotificationsSettings: SmsNotificationsSettingsService; private smsNotificationsSettings: SmsNotificationsSettingsService;
/** /**
* *
@@ -187,6 +186,7 @@ export default class SaleEstimateNotifyBySms {
.findById(saleEstimateId) .findById(saleEstimateId)
.withGraphFetched('customer'); .withGraphFetched('customer');
// Validates the estimate existance.
this.validateEstimateExistance(saleEstimate); this.validateEstimateExistance(saleEstimate);
// Retrieve the current tenant metadata. // Retrieve the current tenant metadata.

View File

@@ -3,7 +3,7 @@ import { ISaleEstimate } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer'; import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils'; import { formatNumber } from 'utils';
export default class SaleEstimateTransfromer extends Transformer { export class SaleEstimateTransfromer extends Transformer {
/** /**
* Include these attributes to sale invoice object. * Include these attributes to sale invoice object.
* @returns {Array} * @returns {Array}

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