mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-05-11 21:34:57 +00:00
add server to monorepo.
This commit is contained in:
52
packages/server/src/api/controllers/Account/index.ts
Normal file
52
packages/server/src/api/controllers/Account/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import AuthenticatedAccount from '@/services/AuthenticatedAccount';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
|
||||
@Service()
|
||||
export default class AccountController extends BaseController {
|
||||
@Inject()
|
||||
accountService: AuthenticatedAccount;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
// Should before build tenant database the user be authorized and
|
||||
// most important than that, should be subscribed to any plan.
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.get('/', this.getAccount);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private getAccount = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const account = await this.accountService.getAccount(tenantId, user);
|
||||
|
||||
return res.status(200).send({ data: account });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
42
packages/server/src/api/controllers/AccountTypes.ts
Normal file
42
packages/server/src/api/controllers/AccountTypes.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import AccountsTypesService from '@/services/Accounts/AccountsTypesServices';
|
||||
|
||||
@Service()
|
||||
export default class AccountsTypesController extends BaseController {
|
||||
@Inject()
|
||||
accountsTypesService: AccountsTypesService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/', asyncMiddleware(this.getAccountTypesList.bind(this)));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts types list.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @return {Response}
|
||||
*/
|
||||
getAccountTypesList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const accountTypes = this.accountsTypesService.getAccountsTypes(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
account_types: this.transfromToResponse(accountTypes, ['label'], req),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
500
packages/server/src/api/controllers/Accounts.ts
Normal file
500
packages/server/src/api/controllers/Accounts.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AccountsApplication } from '@/services/Accounts/AccountsApplication';
|
||||
|
||||
@Service()
|
||||
export default class AccountsController extends BaseController {
|
||||
@Inject()
|
||||
private accountsApplication: AccountsApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/transactions',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Account),
|
||||
[query('account_id').optional().isInt().toInt()],
|
||||
this.asyncMiddleware(this.accountTransactions.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/activate',
|
||||
CheckPolicies(AccountAction.EDIT, AbilitySubject.Account),
|
||||
[...this.accountParamSchema],
|
||||
asyncMiddleware(this.activateAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/inactivate',
|
||||
CheckPolicies(AccountAction.EDIT, AbilitySubject.Account),
|
||||
[...this.accountParamSchema],
|
||||
asyncMiddleware(this.inactivateAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(AccountAction.EDIT, AbilitySubject.Account),
|
||||
[...this.editAccountDTOSchema, ...this.accountParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(AccountAction.CREATE, AbilitySubject.Account),
|
||||
[...this.createAccountDTOSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Account),
|
||||
[...this.accountParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Account),
|
||||
[...this.accountsListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getAccountsList.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(AccountAction.DELETE, AbilitySubject.Account),
|
||||
[...this.accountParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create account DTO Schema validation.
|
||||
*/
|
||||
get createAccountDTOSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('code')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ min: 3, max: 6 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('currency_code').optional(),
|
||||
check('account_type')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('parent_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Account DTO Schema validation.
|
||||
*/
|
||||
get editAccountDTOSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('code')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ min: 3, max: 6 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('account_type')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('parent_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
get accountParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts list validation schema.
|
||||
*/
|
||||
get accountsListSchema() {
|
||||
return [
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
get closingAccountSchema() {
|
||||
return [
|
||||
check('to_account_id').exists().isNumeric().toInt(),
|
||||
check('delete_after_closing').exists().isBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async newAccount(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const accountDTO: IAccountDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const account = await this.accountsApplication.createAccount(
|
||||
tenantId,
|
||||
accountDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: account.id,
|
||||
message: 'The account has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit account details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async editAccount(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: accountId } = req.params;
|
||||
const accountDTO: IAccountDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const account = await this.accountsApplication.editAccount(
|
||||
tenantId,
|
||||
accountId,
|
||||
accountDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: account.id,
|
||||
message: 'The account has been edited successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of the given account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getAccount(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: accountId } = req.params;
|
||||
|
||||
try {
|
||||
const account = await this.accountsApplication.getAccount(
|
||||
tenantId,
|
||||
accountId
|
||||
);
|
||||
return res
|
||||
.status(200)
|
||||
.send({ account: this.transfromToResponse(account) });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteAccount(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: accountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.accountsApplication.deleteAccount(tenantId, accountId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: accountId,
|
||||
message: 'The deleted account has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the given account.
|
||||
* @param {Response} res -
|
||||
* @param {Request} req -
|
||||
* @return {Response}
|
||||
*/
|
||||
async activateAccount(req: Request, res: Response, next: Function) {
|
||||
const { id: accountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.accountsApplication.activateAccount(tenantId, accountId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: accountId,
|
||||
message: 'The account has been activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactive the given account.
|
||||
* @param {Response} res -
|
||||
* @param {Request} req -
|
||||
* @return {Response}
|
||||
*/
|
||||
async inactivateAccount(req: Request, res: Response, next: Function) {
|
||||
const { id: accountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.accountsApplication.inactivateAccount(tenantId, accountId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: accountId,
|
||||
message: 'The account has been inactivated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts datatable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Response}
|
||||
*/
|
||||
public async getAccountsList(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
|
||||
// Filter query.
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
inactiveMode: false,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { accounts, filterMeta } =
|
||||
await this.accountsApplication.getAccounts(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
accounts: this.transfromToResponse(accounts, 'accountTypeLabel', req),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts transactions list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async accountTransactions(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const transactionsFilter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.accountsApplication.getAccountsTransactions(
|
||||
tenantId,
|
||||
transactionsFilter
|
||||
);
|
||||
return res.status(200).send({
|
||||
transactions: this.transfromToResponse(transactions),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms service errors to response.
|
||||
* @param {Error}
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'account_not_found') {
|
||||
return res.boom.notFound('The given account not found.', {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_name_not_unqiue') {
|
||||
return res.boom.badRequest('The given account not unique.', {
|
||||
errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_type_not_found') {
|
||||
return res.boom.badRequest('The given account type not found.', {
|
||||
errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_type_not_allowed_to_changed') {
|
||||
return res.boom.badRequest(
|
||||
'Not allowed to change account type of the account.',
|
||||
{
|
||||
errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'parent_account_not_found') {
|
||||
return res.boom.badRequest('The parent account not found.', {
|
||||
errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'parent_has_different_type') {
|
||||
return res.boom.badRequest('The parent account has different type.', {
|
||||
errors: [
|
||||
{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_code_not_unique') {
|
||||
return res.boom.badRequest('The given account code is not unique.', {
|
||||
errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_has_associated_transactions') {
|
||||
return res.boom.badRequest(
|
||||
'You could not delete account has associated transactions.',
|
||||
{
|
||||
errors: [
|
||||
{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'account_predefined') {
|
||||
return res.boom.badRequest('You could not delete predefined account', {
|
||||
errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'accounts_not_found') {
|
||||
return res.boom.notFound('Some of the given accounts not found.', {
|
||||
errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'predefined_accounts') {
|
||||
return res.boom.badRequest(
|
||||
'Some of the given accounts are predefined.',
|
||||
{ errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] }
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'close_account_and_to_account_not_same_type') {
|
||||
return res.boom.badRequest(
|
||||
'The close account has different root type with to account.',
|
||||
{
|
||||
errors: [
|
||||
{
|
||||
type: 'CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE',
|
||||
code: 1200,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY') {
|
||||
return res.boom.badRequest(
|
||||
'The given account type does not support multi-currency.',
|
||||
{
|
||||
errors: [
|
||||
{ type: 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY', code: 1300 },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT') {
|
||||
return res.boom.badRequest(
|
||||
'You could not add account has currency different on the parent account.',
|
||||
{
|
||||
errors: [
|
||||
{ type: 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT', code: 1400 },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
24
packages/server/src/api/controllers/Agendash.ts
Normal file
24
packages/server/src/api/controllers/Agendash.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Router } from 'express';
|
||||
import basicAuth from 'express-basic-auth';
|
||||
import agendash from 'agendash';
|
||||
import { Container } from 'typedi';
|
||||
import config from '@/config';
|
||||
|
||||
export default class AgendashController {
|
||||
static router() {
|
||||
const router = Router();
|
||||
const agendaInstance = Container.get('agenda');
|
||||
|
||||
router.use(
|
||||
'/dash',
|
||||
basicAuth({
|
||||
users: {
|
||||
[config.agendash.user]: config.agendash.password,
|
||||
},
|
||||
challenge: true,
|
||||
}),
|
||||
agendash(agendaInstance)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
}
|
||||
314
packages/server/src/api/controllers/Authentication.ts
Normal file
314
packages/server/src/api/controllers/Authentication.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { check, ValidationChain } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import countries from 'country-codes-list';
|
||||
import parsePhoneNumber from 'libphonenumber-js';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
import { ILoginDTO, ISystemUser, IRegisterDTO } from '@/interfaces';
|
||||
import { ServiceError, ServiceErrors } from '@/exceptions';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationController extends BaseController {
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
this.loginSchema,
|
||||
this.validationResult,
|
||||
LoginThrottlerMiddleware,
|
||||
asyncMiddleware(this.login.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/register',
|
||||
this.registerSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.register.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/send_reset_password',
|
||||
this.sendResetPasswordSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendResetPassword.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/reset/:token',
|
||||
this.resetPasswordSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.resetPassword.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login schema.
|
||||
*/
|
||||
get loginSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('crediential').exists().isEmail(),
|
||||
check('password').exists().isLength({ min: 5 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register schema.
|
||||
*/
|
||||
get registerSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('first_name')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('last_name')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('email')
|
||||
.exists()
|
||||
.isString()
|
||||
.isEmail()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('phone_number')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.custom(this.phoneNumberValidator)
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('password')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('country')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.custom(this.countryValidator)
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Country validator.
|
||||
*/
|
||||
countryValidator(value, { req }) {
|
||||
const {
|
||||
countries: { whitelist, blacklist },
|
||||
} = config.registration;
|
||||
const foundCountry = countries.findOne('countryCode', value);
|
||||
|
||||
if (!foundCountry) {
|
||||
throw new Error('The country code is invalid.');
|
||||
}
|
||||
if (
|
||||
// Focus with me! In case whitelist is not empty and the given coutry is not
|
||||
// in whitelist throw the error.
|
||||
//
|
||||
// Or in case the blacklist is not empty and the given country exists
|
||||
// in the blacklist throw the goddamn error.
|
||||
(whitelist.length > 0 && whitelist.indexOf(value) === -1) ||
|
||||
(blacklist.length > 0 && blacklist.indexOf(value) !== -1)
|
||||
) {
|
||||
throw new Error('The country code is not supported yet.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phone number validator.
|
||||
*/
|
||||
phoneNumberValidator(value, { req }) {
|
||||
const phoneNumber = parsePhoneNumber(value, req.body.country);
|
||||
|
||||
if (!phoneNumber || !phoneNumber.isValid()) {
|
||||
throw new Error('Phone number is invalid with the given country code.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password schema.
|
||||
*/
|
||||
get resetPasswordSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('password')
|
||||
.exists()
|
||||
.isLength({ min: 5 })
|
||||
.custom((value, { req }) => {
|
||||
if (value !== req.body.confirm_password) {
|
||||
throw new Error("Passwords don't match");
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password validation schema.
|
||||
*/
|
||||
get sendResetPasswordSchema(): ValidationChain[] {
|
||||
return [check('email').exists().isEmail().trim().escape()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user login.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async login(req: Request, res: Response, next: Function): Response {
|
||||
const userDTO: ILoginDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { token, user, tenant } = await this.authService.signIn(
|
||||
userDTO.crediential,
|
||||
userDTO.password
|
||||
);
|
||||
return res.status(200).send({ token, user, tenant });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization register handler.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async register(req: Request, res: Response, next: Function) {
|
||||
const registerDTO: IRegisterDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const registeredUser: ISystemUser = await this.authService.register(
|
||||
registerDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'REGISTER.SUCCESS',
|
||||
message: 'Register organization has been success.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password handler
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async sendResetPassword(req: Request, res: Response, next: Function) {
|
||||
const { email } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.authService.sendResetPassword(email);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 'SEND_RESET_PASSWORD_SUCCESS',
|
||||
message: 'The reset password message has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password handler
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async resetPassword(req: Request, res: Response, next: Function) {
|
||||
const { token } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
try {
|
||||
await this.authService.resetPassword(token, password);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'RESET_PASSWORD_SUCCESS',
|
||||
message: 'The password has been reset successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the service errors.
|
||||
*/
|
||||
handlerErrors(error, req: Request, res: Response, next: Function) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (
|
||||
['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1
|
||||
) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_INACTIVE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_INACTIVE', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'TOKEN_INVALID' ||
|
||||
error.errorType === 'TOKEN_EXPIRED'
|
||||
) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'TOKEN_INVALID', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EMAIL_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 500 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
if (error instanceof ServiceErrors) {
|
||||
const errorReasons = [];
|
||||
|
||||
if (error.hasType('PHONE_NUMBER_EXISTS')) {
|
||||
errorReasons.push({ type: 'PHONE_NUMBER_EXISTS', code: 100 });
|
||||
}
|
||||
if (error.hasType('EMAIL_EXISTS')) {
|
||||
errorReasons.push({ type: 'EMAIL.EXISTS', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
140
packages/server/src/api/controllers/BaseController.ts
Normal file
140
packages/server/src/api/controllers/BaseController.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Response, Request, NextFunction } from 'express';
|
||||
import { matchedData, validationResult } from 'express-validator';
|
||||
import accepts from 'accepts';
|
||||
import { isArray, drop, first, camelCase, snakeCase, omit, set, get } from 'lodash';
|
||||
import { mapKeysDeep } from 'utils';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
|
||||
export default class BaseController {
|
||||
/**
|
||||
* Converts plain object keys to cameCase style.
|
||||
* @param {Object} data
|
||||
*/
|
||||
protected dataToCamelCase(data) {
|
||||
return mapKeysDeep(data, (v, k) => camelCase(k));
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the body data from validation schema.
|
||||
* @param {Request} req
|
||||
* @param options
|
||||
*/
|
||||
protected matchedBodyData(req: Request, options: any = {}) {
|
||||
const data = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
...omit(options, ['locations']), // override any propery except locations.
|
||||
});
|
||||
return this.dataToCamelCase(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the query data from validation schema.
|
||||
* @param {Request} req
|
||||
*/
|
||||
protected matchedQueryData(req: Request) {
|
||||
const data = matchedData(req, {
|
||||
locations: ['query'],
|
||||
});
|
||||
return this.dataToCamelCase(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate validation schema middleware.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
protected validationResult(req: Request, res: Response, next: NextFunction) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets localization to response object by the given path.
|
||||
* @param {Response} response -
|
||||
* @param {string} path -
|
||||
* @param {Request} req -
|
||||
*/
|
||||
private setLocalizationByPath(
|
||||
response: any,
|
||||
path: string,
|
||||
req: Request,
|
||||
) {
|
||||
const DOT = '.';
|
||||
|
||||
if (isArray(response)) {
|
||||
response.forEach((va) => {
|
||||
const currentPath = first(path.split(DOT));
|
||||
const value = get(va, currentPath);
|
||||
|
||||
if (isArray(value)) {
|
||||
const nextPath = drop(path.split(DOT)).join(DOT);
|
||||
this.setLocalizationByPath(value, nextPath, req);
|
||||
} else {
|
||||
set(va, path, req.__(value));
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const value = get(response, path);
|
||||
set(response, path, req.__(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the given data to response.
|
||||
* @param {any} data
|
||||
*/
|
||||
protected transfromToResponse(
|
||||
data: any,
|
||||
translatable?: string | string[],
|
||||
req?: Request
|
||||
) {
|
||||
const response = mapKeysDeep(data, (v, k) => snakeCase(k));
|
||||
|
||||
if (translatable) {
|
||||
const translatables = Array.isArray(translatable)
|
||||
? translatable
|
||||
: [translatable];
|
||||
|
||||
translatables.forEach((path) => {
|
||||
this.setLocalizationByPath(response, path, req);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async middleware.
|
||||
* @param {function} callback
|
||||
*/
|
||||
protected asyncMiddleware(callback) {
|
||||
return asyncMiddleware(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Request} req
|
||||
* @returns
|
||||
*/
|
||||
protected accepts(req) {
|
||||
return accepts(req);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {string[]} types
|
||||
* @returns {string}
|
||||
*/
|
||||
protected acceptTypes(req: Request, types: string[]) {
|
||||
return this.accepts(req).types(types);
|
||||
}
|
||||
}
|
||||
335
packages/server/src/api/controllers/Branches/index.ts
Normal file
335
packages/server/src/api/controllers/Branches/index.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { check, param } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { Features, ICreateBranchDTO, IEditBranchDTO } from '@/interfaces';
|
||||
import { BranchesApplication } from '@/services/Branches/BranchesApplication';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { FeatureActivationGuard } from '@/api/middleware/FeatureActivationGuard';
|
||||
|
||||
@Service()
|
||||
export class BranchesController extends BaseController {
|
||||
@Inject()
|
||||
branchesApplication: BranchesApplication;
|
||||
|
||||
/**
|
||||
* Branches routes.
|
||||
* @returns {Router}
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/activate',
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.activateBranches),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[
|
||||
check('name').exists(),
|
||||
check('code').optional({ nullable: true }),
|
||||
|
||||
check('address').optional({ nullable: true }),
|
||||
check('city').optional({ nullable: true }),
|
||||
check('country').optional({ nullable: true }),
|
||||
|
||||
check('phone_number').optional({ nullable: true }),
|
||||
check('email').optional({ nullable: true }).isEmail(),
|
||||
check('website').optional({ nullable: true }).isURL(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.createBranch),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[
|
||||
param('id').exists().isInt().toInt(),
|
||||
check('name').exists(),
|
||||
check('code').optional({ nullable: true }),
|
||||
|
||||
check('address').optional({ nullable: true }),
|
||||
check('city').optional({ nullable: true }),
|
||||
check('country').optional({ nullable: true }),
|
||||
|
||||
check('phone_number').optional({ nullable: true }),
|
||||
check('email').optional({ nullable: true }).isEmail(),
|
||||
check('website').optional({ nullable: true }).isURL(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.editBranch),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/mark-primary',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.markBranchAsPrimary),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteBranch),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getBranch),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getBranches),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new branch.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public createBranch = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const createBranchDTO: ICreateBranchDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const branch = await this.branchesApplication.createBranch(
|
||||
tenantId,
|
||||
createBranchDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: branch.id,
|
||||
message: 'The branch has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits the given branch.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public editBranch = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: branchId } = req.params;
|
||||
const editBranchDTO: IEditBranchDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const branch = await this.branchesApplication.editBranch(
|
||||
tenantId,
|
||||
branchId,
|
||||
editBranchDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: branch.id,
|
||||
message: 'The branch has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given branch.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public deleteBranch = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: branchId } = req.params;
|
||||
|
||||
try {
|
||||
await this.branchesApplication.deleteBranch(tenantId, branchId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: branchId,
|
||||
message: 'The branch has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves specific branch.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public getBranch = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: branchId } = req.params;
|
||||
|
||||
try {
|
||||
const branch = await this.branchesApplication.getBranch(
|
||||
tenantId,
|
||||
branchId
|
||||
);
|
||||
return res.status(200).send({ branch });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves branches list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public getBranches = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const branches = await this.branchesApplication.getBranches(tenantId);
|
||||
|
||||
return res.status(200).send({ branches });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Activates the multi-branches feature.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public activateBranches = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.branchesApplication.activateBranches(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Multi-branches feature has been activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks the given branch as primary.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public markBranchAsPrimary = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: branchId } = req.params;
|
||||
|
||||
try {
|
||||
await this.branchesApplication.markBranchAsPrimary(tenantId, branchId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: branchId,
|
||||
message: 'The branch has been marked as primary.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'BRANCH_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BRANCH_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MUTLI_BRANCHES_ALREADY_ACTIVATED') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'MUTLI_BRANCHES_ALREADY_ACTIVATED', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COULD_NOT_DELETE_ONLY_BRANCH') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COULD_NOT_DELETE_ONLY_BRANCH', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BRANCH_CODE_NOT_UNIQUE') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BRANCH_CODE_NOT_UNIQUE', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BRANCH_HAS_ASSOCIATED_TRANSACTIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'BRANCH_HAS_ASSOCIATED_TRANSACTIONS', code: 500 },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Service, Inject, Container } from 'typedi';
|
||||
import { Router } from 'express';
|
||||
import CommandCashflowTransaction from './NewCashflowTransaction';
|
||||
import DeleteCashflowTransaction from './DeleteCashflowTransaction';
|
||||
import GetCashflowTransaction from './GetCashflowTransaction';
|
||||
import GetCashflowAccounts from './GetCashflowAccounts';
|
||||
|
||||
@Service()
|
||||
export default class CashflowController {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(Container.get(GetCashflowTransaction).router());
|
||||
router.use(Container.get(GetCashflowAccounts).router());
|
||||
router.use(Container.get(CommandCashflowTransaction).router());
|
||||
router.use(Container.get(DeleteCashflowTransaction).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DeleteCashflowTransactionService from '../../../services/Cashflow/DeleteCashflowTransactionService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class DeleteCashflowTransaction extends BaseController {
|
||||
@Inject()
|
||||
deleteCashflowService: DeleteCashflowTransactionService;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.delete(
|
||||
'/transactions/:transactionId',
|
||||
CheckPolicies(CashflowAction.Delete, AbilitySubject.Cashflow),
|
||||
[param('transactionId').exists().isInt().toInt()],
|
||||
this.asyncMiddleware(this.deleteCashflowTransaction),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private deleteCashflowTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
|
||||
try {
|
||||
const { oldCashflowTransaction } =
|
||||
await this.deleteCashflowService.deleteCashflowTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: oldCashflowTransaction.id,
|
||||
message: 'The cashflow transaction has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param error
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'CASHFLOW_TRANSACTION_NOT_FOUND') {
|
||||
return res.boom.badRequest(
|
||||
'The given cashflow transaction not found.',
|
||||
{
|
||||
errors: [{ type: 'CASHFLOW_TRANSACTION_NOT_FOUND', code: 100 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param, query } from 'express-validator';
|
||||
import GetCashflowAccountsService from '@/services/Cashflow/GetCashflowAccountsService';
|
||||
import BaseController from '../BaseController';
|
||||
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class GetCashflowAccounts extends BaseController {
|
||||
@Inject()
|
||||
getCashflowAccountsService: GetCashflowAccountsService;
|
||||
|
||||
@Inject()
|
||||
getCashflowTransactionsService: GetCashflowTransactionsService;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/accounts',
|
||||
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
|
||||
[
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
],
|
||||
this.asyncMiddleware(this.getCashflowAccounts),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow accounts.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getCashflowAccounts = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
// Filter query.
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
inactiveMode: false,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const cashflowAccounts =
|
||||
await this.getCashflowAccountsService.getCashflowAccounts(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
cashflow_accounts: this.transfromToResponse(cashflowAccounts),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param {Error} error - Error.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class GetCashflowAccounts extends BaseController {
|
||||
@Inject()
|
||||
getCashflowTransactionsService: GetCashflowTransactionsService;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/transactions/:transactionId',
|
||||
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
|
||||
[param('transactionId').exists().isInt().toInt()],
|
||||
this.asyncMiddleware(this.getCashflowTransaction),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getCashflowTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
|
||||
try {
|
||||
const cashflowTransaction =
|
||||
await this.getCashflowTransactionsService.getCashflowTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
cashflow_transaction: this.transfromToResponse(cashflowTransaction),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param {Error} error - Error.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'CASHFLOW_TRANSACTION_NOT_FOUND') {
|
||||
return res.boom.badRequest(
|
||||
'The given cashflow tranasction not found.',
|
||||
{
|
||||
errors: [{ type: 'CASHFLOW_TRANSACTION_NOT_FOUND', code: 200 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'ACCOUNT_ID_HAS_INVALID_TYPE') {
|
||||
return res.boom.badRequest(
|
||||
'The given cashflow account has invalid type.',
|
||||
{
|
||||
errors: [{ type: 'ACCOUNT_ID_HAS_INVALID_TYPE', code: 300 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest('The given account not found.', {
|
||||
errors: [{ type: 'ACCOUNT_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { check } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import BaseController from '../BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class NewCashflowTransactionController extends BaseController {
|
||||
@Inject()
|
||||
private newCashflowTranscationService: NewCashflowTransactionService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/transactions',
|
||||
CheckPolicies(CashflowAction.Create, AbilitySubject.Cashflow),
|
||||
this.newTransactionValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.newCashflowTransaction),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* New cashflow transaction validation schema.
|
||||
*/
|
||||
get newTransactionValidationSchema() {
|
||||
return [
|
||||
check('date').exists().isISO8601().toDate(),
|
||||
check('reference_no').optional({ nullable: true }).trim().escape(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ min: 3 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('transaction_type').exists(),
|
||||
|
||||
check('amount').exists().isFloat().toFloat(),
|
||||
check('cashflow_account_id').exists().isInt().toInt(),
|
||||
check('credit_account_id').exists().isInt().toInt(),
|
||||
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('publish').default(false).isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new cashflow transaction.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private newCashflowTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, userId } = req;
|
||||
const ownerContributionDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { cashflowTransaction } =
|
||||
await this.newCashflowTranscationService.newCashflowTransaction(
|
||||
tenantId,
|
||||
ownerContributionDTO,
|
||||
userId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: cashflowTransaction.id,
|
||||
message: 'New cashflow transaction has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the service errors.
|
||||
* @param error
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'CASHFLOW_ACCOUNTS_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest('Cashflow accounts ids not found.', {
|
||||
errors: [{ type: 'CASHFLOW_ACCOUNTS_IDS_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_ACCOUNTS_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest('Credit accounts ids not found.', {
|
||||
errors: [{ type: 'CREDIT_ACCOUNTS_IDS_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE') {
|
||||
return res.boom.badRequest('Cashflow .', {
|
||||
errors: [{ type: 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE') {
|
||||
return res.boom.badRequest(
|
||||
'Cashflow accounts should be cash or bank type.',
|
||||
{
|
||||
errors: [{ type: 'CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE', code: 300 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'CASHFLOW_TRANSACTION_NOT_FOUND') {
|
||||
return res.boom.badRequest('Cashflow transaction not found.', {
|
||||
errors: [{ type: 'CASHFLOW_TRANSACTION_NOT_FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
404
packages/server/src/api/controllers/Contacts/Contacts.ts
Normal file
404
packages/server/src/api/controllers/Contacts/Contacts.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import { check, param, query, body, ValidationChain } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import ContactsService from '@/services/Contacts/ContactsService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
|
||||
@Service()
|
||||
export default class ContactsController extends BaseController {
|
||||
@Inject()
|
||||
contactsService: ContactsService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/auto-complete',
|
||||
[...this.autocompleteQuerySchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.autocompleteContacts.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getContact.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/:id/inactivate',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.inactivateContact.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/activate',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.activateContact.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-complete list query validation schema.
|
||||
*/
|
||||
get autocompleteQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('limit').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details of the given contact.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async getContact(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
const contact = await this.contactsService.getContact(
|
||||
tenantId,
|
||||
contactId
|
||||
);
|
||||
return res.status(200).send({
|
||||
customer: this.transfromToResponse(contact),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve auto-complete contacts list.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async autocompleteContacts(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
filterRoles: [],
|
||||
sortOrder: 'asc',
|
||||
columnSortBy: 'display_name',
|
||||
limit: 10,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
try {
|
||||
const contacts = await this.contactsService.autocompleteContacts(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({ contacts });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get contactDTOSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('salutation')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('first_name')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('last_name')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('company_name')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('display_name')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('email')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.normalizeEmail()
|
||||
.isEmail()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('website')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.isURL()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('work_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('personal_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('billing_address_1')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_2')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_city')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_country')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_email')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isEmail()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_postcode')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_state')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('shipping_address_1')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_2')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_city')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_country')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_email')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isEmail()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_postcode')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_state')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('note')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('active').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact new DTO schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get contactNewDTOSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('opening_balance')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toInt(),
|
||||
check('opening_balance_exchange_rate')
|
||||
.default(1)
|
||||
.isFloat({ gt: 0 })
|
||||
.toFloat(),
|
||||
body('opening_balance_at')
|
||||
.if(body('opening_balance').exists())
|
||||
.exists()
|
||||
.isISO8601(),
|
||||
check('opening_balance_branch_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact edit DTO schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get contactEditDTOSchema(): ValidationChain[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get specificContactSchema(): ValidationChain[] {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given contact.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async activateContact(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.contactsService.activateContact(tenantId, contactId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The given contact activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivate the given contact.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async inactivateContact(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.contactsService.inactivateContact(tenantId, contactId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The given contact inactivated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CONTACT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CONTACT_ALREADY_ACTIVE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CONTACT_ALREADY_ACTIVE', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CONTACT_ALREADY_INACTIVE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CONTACT_ALREADY_INACTIVE', code: 800 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
351
packages/server/src/api/controllers/Contacts/Customers.ts
Normal file
351
packages/server/src/api/controllers/Contacts/Customers.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { check, query } from 'express-validator';
|
||||
import ContactsController from '@/api/controllers/Contacts/Contacts';
|
||||
import CustomersService from '@/services/Contacts/CustomersService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
ICustomerNewDTO,
|
||||
ICustomerEditDTO,
|
||||
AbilitySubject,
|
||||
CustomerAction,
|
||||
} from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { CustomersApplication } from '@/services/Contacts/Customers/CustomersApplication';
|
||||
|
||||
@Service()
|
||||
export default class CustomersController extends ContactsController {
|
||||
@Inject()
|
||||
private customersApplication: CustomersApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(CustomerAction.Create, AbilitySubject.Customer),
|
||||
[
|
||||
...this.contactDTOSchema,
|
||||
...this.contactNewDTOSchema,
|
||||
...this.customerDTOSchema,
|
||||
...this.createCustomerDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/opening_balance',
|
||||
CheckPolicies(CustomerAction.Edit, AbilitySubject.Customer),
|
||||
[
|
||||
...this.specificContactSchema,
|
||||
check('opening_balance').exists().isNumeric().toFloat(),
|
||||
check('opening_balance_at').optional().isISO8601(),
|
||||
check('opening_balance_exchange_rate')
|
||||
.default(1)
|
||||
.isFloat({ gt: 0 })
|
||||
.toFloat(),
|
||||
check('opening_balance_branch_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editOpeningBalanceCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(CustomerAction.Edit, AbilitySubject.Customer),
|
||||
[
|
||||
...this.contactDTOSchema,
|
||||
...this.contactEditDTOSchema,
|
||||
...this.customerDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(CustomerAction.Delete, AbilitySubject.Customer),
|
||||
[...this.specificContactSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(CustomerAction.View, AbilitySubject.Customer),
|
||||
[...this.validateListQuerySchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getCustomersList.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(CustomerAction.View, AbilitySubject.Customer),
|
||||
[...this.specificContactSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer DTO schema.
|
||||
*/
|
||||
get customerDTOSchema() {
|
||||
return [
|
||||
check('customer_type')
|
||||
.exists()
|
||||
.isIn(['business', 'individual'])
|
||||
.trim()
|
||||
.escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create customer DTO schema.
|
||||
*/
|
||||
get createCustomerDTOSchema() {
|
||||
return [
|
||||
check('currency_code')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: 3 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* List param query schema.
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('view_slug').optional().isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new customer.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async newCustomer(req: Request, res: Response, next: NextFunction) {
|
||||
const contactDTO: ICustomerNewDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const contact = await this.customersApplication.createCustomer(
|
||||
tenantId,
|
||||
contactDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contact.id,
|
||||
message: 'The customer has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given customer details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editCustomer(req: Request, res: Response, next: NextFunction) {
|
||||
const contactDTO: ICustomerEditDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.customersApplication.editCustomer(
|
||||
tenantId,
|
||||
contactId,
|
||||
contactDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The customer has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the opening balance of the given customer.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async editOpeningBalanceCustomer(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: customerId } = req.params;
|
||||
const openingBalanceEditDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.customersApplication.editOpeningBalance(
|
||||
tenantId,
|
||||
customerId,
|
||||
openingBalanceEditDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: customerId,
|
||||
message:
|
||||
'The opening balance of the given customer has been changed successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details of the given customer id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getCustomer(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
const customer = await this.customersApplication.getCustomer(
|
||||
tenantId,
|
||||
contactId,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
customer: this.transfromToResponse(customer),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given customer from the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteCustomer(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.customersApplication.deleteCustomer(tenantId, contactId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The customer has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve customers paginated and filterable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getCustomersList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const filter = {
|
||||
inactiveMode: false,
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { customers, pagination, filterMeta } =
|
||||
await this.customersApplication.getCustomers(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
customers: this.transfromToResponse(customers),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contacts_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_HAS_TRANSACTIONS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_HAS_TRANSACTIONS', code: 600 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
332
packages/server/src/api/controllers/Contacts/Vendors.ts
Normal file
332
packages/server/src/api/controllers/Contacts/Vendors.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { body, query, ValidationChain, check } from 'express-validator';
|
||||
|
||||
import ContactsController from '@/api/controllers/Contacts/Contacts';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
IVendorNewDTO,
|
||||
IVendorEditDTO,
|
||||
IVendorsFilter,
|
||||
AbilitySubject,
|
||||
VendorAction,
|
||||
} from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { VendorsApplication } from '@/services/Contacts/Vendors/VendorsApplication';
|
||||
|
||||
@Service()
|
||||
export default class VendorsController extends ContactsController {
|
||||
@Inject()
|
||||
private vendorsApplication: VendorsApplication;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(VendorAction.Create, AbilitySubject.Vendor),
|
||||
[
|
||||
...this.contactDTOSchema,
|
||||
...this.contactNewDTOSchema,
|
||||
...this.vendorDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/opening_balance',
|
||||
CheckPolicies(VendorAction.Edit, AbilitySubject.Vendor),
|
||||
[
|
||||
...this.specificContactSchema,
|
||||
check('opening_balance').exists().isNumeric().toFloat(),
|
||||
check('opening_balance_at').optional().isISO8601(),
|
||||
check('opening_balance_exchange_rate')
|
||||
.default(1)
|
||||
.isFloat({ gt: 0 })
|
||||
.toFloat(),
|
||||
check('opening_balance_branch_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editOpeningBalanceVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(VendorAction.Edit, AbilitySubject.Vendor),
|
||||
[
|
||||
...this.contactDTOSchema,
|
||||
...this.contactEditDTOSchema,
|
||||
...this.vendorDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(VendorAction.Delete, AbilitySubject.Vendor),
|
||||
[...this.specificContactSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(VendorAction.View, AbilitySubject.Vendor),
|
||||
[...this.specificContactSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(VendorAction.View, AbilitySubject.Vendor),
|
||||
[...this.vendorsListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getVendorsList.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendor DTO schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get vendorDTOSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('currency_code')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ min: 3, max: 3 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendors datatable list validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get vendorsListSchema() {
|
||||
return [
|
||||
query('view_slug').optional().isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new vendor.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async newVendor(req: Request, res: Response, next: NextFunction) {
|
||||
const contactDTO: IVendorNewDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const vendor = await this.vendorsApplication.createVendor(
|
||||
tenantId,
|
||||
contactDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: vendor.id,
|
||||
message: 'The vendor has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given vendor details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editVendor(req: Request, res: Response, next: NextFunction) {
|
||||
const contactDTO: IVendorEditDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.vendorsApplication.editVendor(
|
||||
tenantId,
|
||||
contactId,
|
||||
contactDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The vendor has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the opening balance of the given vendor.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async editOpeningBalanceVendor(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: vendorId } = req.params;
|
||||
const editOpeningBalanceDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.vendorsApplication.editOpeningBalance(
|
||||
tenantId,
|
||||
vendorId,
|
||||
editOpeningBalanceDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: vendorId,
|
||||
message:
|
||||
'The opening balance of the given vendor has been changed successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given vendor from the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteVendor(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.vendorsApplication.deleteVendor(tenantId, contactId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The vendor has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details of the given vendor id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getVendor(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: vendorId } = req.params;
|
||||
|
||||
try {
|
||||
const vendor = await this.vendorsApplication.getVendor(
|
||||
tenantId,
|
||||
vendorId,
|
||||
user
|
||||
);
|
||||
return res.status(200).send(this.transfromToResponse({ vendor }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve vendors datatable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getVendorsList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const vendorsFilter: IVendorsFilter = {
|
||||
inactiveMode: false,
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { vendors, pagination, filterMeta } =
|
||||
await this.vendorsApplication.getVendors(tenantId, vendorsFilter);
|
||||
|
||||
return res.status(200).send({
|
||||
vendors: this.transfromToResponse(vendors),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error -
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contacts_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDORS.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_HAS_TRANSACTIONS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDOR_HAS_TRANSACTIONS', code: 600 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
211
packages/server/src/api/controllers/Currencies.ts
Normal file
211
packages/server/src/api/controllers/Currencies.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from './BaseController';
|
||||
import CurrenciesService from '@/services/Currencies/CurrenciesService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export default class CurrenciesController extends BaseController {
|
||||
@Inject()
|
||||
currenciesService: CurrenciesService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
[...this.listSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.all.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
[...this.currencyDTOSchemaValidation],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newCurrency.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[...this.currencyIdParamSchema, ...this.currencyEditDTOSchemaValidation],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editCurrency.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.delete(
|
||||
'/:currency_code',
|
||||
[...this.currencyParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteCurrency.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
get currencyDTOSchemaValidation(): ValidationChain[] {
|
||||
return [
|
||||
check('currency_name').exists().trim(),
|
||||
check('currency_code').exists().trim(),
|
||||
check('currency_sign').exists().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
get currencyEditDTOSchemaValidation(): ValidationChain[] {
|
||||
return [
|
||||
check('currency_name').exists().trim(),
|
||||
check('currency_sign').exists().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
get currencyIdParamSchema(): ValidationChain[] {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
get currencyParamSchema(): ValidationChain[] {
|
||||
return [param('currency_code').exists().trim().escape()];
|
||||
}
|
||||
|
||||
get listSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all registered currency details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async all(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const currencies = await this.currenciesService.listCurrencies(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
currencies: this.transfromToResponse(currencies),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new currency on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async newCurrency(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
const currencyDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.currenciesService.newCurrency(tenantId, currencyDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
currency_code: currencyDTO.currencyCode,
|
||||
message: 'The currency has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits details of the given currency.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteCurrency(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
const { currency_code: currencyCode } = req.params;
|
||||
|
||||
try {
|
||||
await this.currenciesService.deleteCurrency(tenantId, currencyCode);
|
||||
return res.status(200).send({
|
||||
currency_code: currencyCode,
|
||||
message: 'The currency has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the currency.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editCurrency(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
const { id: currencyId } = req.params;
|
||||
const editCurrencyDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const currency = await this.currenciesService.editCurrency(
|
||||
tenantId,
|
||||
currencyId,
|
||||
editCurrencyDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
currency_code: currency.currencyCode,
|
||||
message: 'The currency has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles currencies service error.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'currency_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CURRENCY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'currency_code_exists') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{
|
||||
type: 'CURRENCY_CODE_EXISTS',
|
||||
message: 'The given currency code is already exists.',
|
||||
code: 200,
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CANNOT_DELETE_BASE_CURRENCY') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'CANNOT_DELETE_BASE_CURRENCY',
|
||||
code: 300,
|
||||
message: 'Cannot delete the base currency.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
47
packages/server/src/api/controllers/Dashboard/index.ts
Normal file
47
packages/server/src/api/controllers/Dashboard/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import DashboardService from '@/services/Dashboard/DashboardService';
|
||||
|
||||
@Service()
|
||||
export default class DashboardMetaController {
|
||||
@Inject()
|
||||
dashboardService: DashboardService;
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/boot', this.getDashboardBoot);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the dashboard boot meta.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
getDashboardBoot = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const authorizedUser = req.user;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const meta = await this.dashboardService.getBootMeta(
|
||||
tenantId,
|
||||
authorizedUser
|
||||
);
|
||||
|
||||
return res.status(200).send({ meta });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
220
packages/server/src/api/controllers/ExchangeRates.ts
Normal file
220
packages/server/src/api/controllers/ExchangeRates.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from './BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import ExchangeRatesService from '@/services/ExchangeRates/ExchangeRatesService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
|
||||
@Service()
|
||||
export default class ExchangeRatesController extends BaseController {
|
||||
@Inject()
|
||||
exchangeRatesService: ExchangeRatesService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
[...this.exchangeRatesListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.exchangeRates.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.handleServiceError,
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
[...this.exchangeRateDTOSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.addExchangeRate.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[...this.exchangeRateEditDTOSchema, ...this.exchangeRateIdSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editExchangeRate.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[...this.exchangeRateIdSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteExchangeRate.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
get exchangeRatesListSchema() {
|
||||
return [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
get exchangeRateDTOSchema() {
|
||||
return [
|
||||
check('exchange_rate').exists().isNumeric().toFloat(),
|
||||
check('currency_code').exists().trim().escape(),
|
||||
check('date').exists().isISO8601(),
|
||||
];
|
||||
}
|
||||
|
||||
get exchangeRateEditDTOSchema() {
|
||||
return [check('exchange_rate').exists().isNumeric().toFloat()];
|
||||
}
|
||||
|
||||
get exchangeRateIdSchema() {
|
||||
return [param('id').isNumeric().toInt()];
|
||||
}
|
||||
|
||||
get exchangeRatesIdsSchema() {
|
||||
return [
|
||||
query('ids').isArray({ min: 2 }),
|
||||
query('ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve exchange rates.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async exchangeRates(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
filterRoles: [],
|
||||
columnSortBy: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
if (filter.stringifiedFilterRoles) {
|
||||
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
|
||||
}
|
||||
try {
|
||||
const exchangeRates = await this.exchangeRatesService.listExchangeRates(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({ exchange_rates: exchangeRates });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new exchange rate on the given date.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async addExchangeRate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const exchangeRateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const exchangeRate = await this.exchangeRatesService.newExchangeRate(
|
||||
tenantId,
|
||||
exchangeRateDTO
|
||||
);
|
||||
return res.status(200).send({ id: exchangeRate.id });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the given exchange rate.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editExchangeRate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: exchangeRateId } = req.params;
|
||||
const exchangeRateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const exchangeRate = await this.exchangeRatesService.editExchangeRate(
|
||||
tenantId,
|
||||
exchangeRateId,
|
||||
exchangeRateDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: exchangeRateId,
|
||||
message: 'The exchange rate has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given exchange rate from the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteExchangeRate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: exchangeRateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.exchangeRatesService.deleteExchangeRate(
|
||||
tenantId,
|
||||
exchangeRateId
|
||||
);
|
||||
return res.status(200).send({ id: exchangeRateId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
456
packages/server/src/api/controllers/Expenses/Expenses.ts
Normal file
456
packages/server/src/api/controllers/Expenses/Expenses.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ExpenseAction,
|
||||
IExpenseCreateDTO,
|
||||
IExpenseEditDTO,
|
||||
} from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ExpensesApplication } from '@/services/Expenses/ExpensesApplication';
|
||||
|
||||
@Service()
|
||||
export class ExpensesController extends BaseController {
|
||||
@Inject()
|
||||
private expensesApplication: ExpensesApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(ExpenseAction.Create, AbilitySubject.Expense),
|
||||
[...this.expenseDTOSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/publish',
|
||||
CheckPolicies(ExpenseAction.Edit, AbilitySubject.Expense),
|
||||
[...this.expenseParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.publishExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(ExpenseAction.Edit, AbilitySubject.Expense),
|
||||
[...this.editExpenseDTOSchema, ...this.expenseParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(ExpenseAction.Delete, AbilitySubject.Expense),
|
||||
[...this.expenseParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ExpenseAction.View, AbilitySubject.Expense),
|
||||
[...this.expensesListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getExpensesList.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(ExpenseAction.View, AbilitySubject.Expense),
|
||||
[this.expenseParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expense DTO schema.
|
||||
*/
|
||||
get expenseDTOSchema() {
|
||||
return [
|
||||
check('reference_no')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('payment_date').exists().isISO8601().toDate(),
|
||||
check('payment_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('currency_code').optional().isString().isLength({ max: 3 }),
|
||||
check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
check('payee_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('categories').exists().isArray({ min: 1 }),
|
||||
check('categories.*.index')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.expense_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.amount')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3
|
||||
.toFloat(),
|
||||
check('categories.*.description')
|
||||
.optional()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||
check('categories.*.project_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit expense validation schema.
|
||||
*/
|
||||
get editExpenseDTOSchema() {
|
||||
return [
|
||||
check('reference_no')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('payment_date').exists().isISO8601().toDate(),
|
||||
check('payment_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('currency_code').optional().isString().isLength({ max: 3 }),
|
||||
check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
check('payee_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('categories').exists().isArray({ min: 1 }),
|
||||
check('categories.*.id').optional().isNumeric().toInt(),
|
||||
check('categories.*.index')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.expense_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.amount')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3
|
||||
.toFloat(),
|
||||
check('categories.*.description')
|
||||
.optional()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||
check('categories.*.project_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expense param validation schema.
|
||||
*/
|
||||
get expenseParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expenses list validation schema.
|
||||
*/
|
||||
get expensesListSchema() {
|
||||
return [
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expense on
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async newExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const expenseDTO: IExpenseCreateDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const expense = await this.expensesApplication.createExpense(
|
||||
tenantId,
|
||||
expenseDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: expense.id,
|
||||
message: 'The expense has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits details of the given expense.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: expenseId } = req.params;
|
||||
const expenseDTO: IExpenseEditDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
await this.expensesApplication.editExpense(
|
||||
tenantId,
|
||||
expenseId,
|
||||
expenseDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: expenseId,
|
||||
message: 'The expense has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given expense.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: expenseId } = req.params;
|
||||
|
||||
try {
|
||||
await this.expensesApplication.deleteExpense(tenantId, expenseId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: expenseId,
|
||||
message: 'The expense has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishs the given expense.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async publishExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: expenseId } = req.params;
|
||||
|
||||
try {
|
||||
await this.expensesApplication.publishExpense(tenantId, expenseId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: expenseId,
|
||||
message: 'The expense has been published successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve expneses list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getExpensesList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { expenses, pagination, filterMeta } =
|
||||
await this.expensesApplication.getExpenses(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
expenses: this.transfromToResponse(expenses),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve expense details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: expenseId } = req.params;
|
||||
|
||||
try {
|
||||
const expense = await this.expensesApplication.getExpense(
|
||||
tenantId,
|
||||
expenseId
|
||||
);
|
||||
return res.status(200).send(this.transfromToResponse({ expense }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform service errors to api response errors.
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'expense_not_found') {
|
||||
return res.boom.badRequest('Expense not found.', {
|
||||
errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EXPENSES_NOT_FOUND') {
|
||||
return res.boom.badRequest('Expenses not found.', {
|
||||
errors: [{ type: 'EXPENSES_NOT_FOUND', code: 110 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'total_amount_equals_zero') {
|
||||
return res.boom.badRequest('Expense total should not equal zero.', {
|
||||
errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'payment_account_not_found') {
|
||||
return res.boom.badRequest('Payment account not found.', {
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'some_expenses_not_found') {
|
||||
return res.boom.badRequest('Some expense accounts not found.', {
|
||||
errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'payment_account_has_invalid_type') {
|
||||
return res.boom.badRequest('Payment account has invalid type.', {
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'expenses_account_has_invalid_type') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'EXPENSES.ACCOUNT.HAS.INVALID.TYPE', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'expense_already_published') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'EXPENSE_ALREADY_PUBLISHED', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CONTACT_NOT_FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EXPENSE_HAS_ASSOCIATED_LANDED_COST') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', code: 1000 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES'
|
||||
) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES',
|
||||
code: 1100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
15
packages/server/src/api/controllers/Expenses/index.ts
Normal file
15
packages/server/src/api/controllers/Expenses/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
import { Router } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
import { ExpensesController } from './Expenses';
|
||||
|
||||
@Service()
|
||||
export default class ExpensesBaseController {
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/', Container.get(ExpensesController).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
107
packages/server/src/api/controllers/FinancialStatements.ts
Normal file
107
packages/server/src/api/controllers/FinancialStatements.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Router } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
|
||||
import BalanceSheetController from './FinancialStatements/BalanceSheet';
|
||||
import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet';
|
||||
import GeneralLedgerController from './FinancialStatements/GeneralLedger';
|
||||
import JournalSheetController from './FinancialStatements/JournalSheet';
|
||||
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||
import ARAgingSummary from './FinancialStatements/ARAgingSummary';
|
||||
import APAgingSummary from './FinancialStatements/APAgingSummary';
|
||||
import PurchasesByItemsController from './FinancialStatements/PurchasesByItem';
|
||||
import SalesByItemsController from './FinancialStatements/SalesByItems';
|
||||
import InventoryValuationController from './FinancialStatements/InventoryValuationSheet';
|
||||
import CustomerBalanceSummaryController from './FinancialStatements/CustomerBalanceSummary';
|
||||
import VendorBalanceSummaryController from './FinancialStatements/VendorBalanceSummary';
|
||||
import TransactionsByCustomers from './FinancialStatements/TransactionsByCustomers';
|
||||
import TransactionsByVendors from './FinancialStatements/TransactionsByVendors';
|
||||
import CashFlowStatementController from './FinancialStatements/CashFlow/CashFlow';
|
||||
import InventoryDetailsController from './FinancialStatements/InventoryDetails';
|
||||
import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference';
|
||||
import CashflowAccountTransactions from './FinancialStatements/CashflowAccountTransactions';
|
||||
import ProjectProfitabilityController from './FinancialStatements/ProjectProfitabilitySummary';
|
||||
|
||||
@Service()
|
||||
export default class FinancialStatementsService {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(
|
||||
'/balance_sheet',
|
||||
Container.get(BalanceSheetController).router()
|
||||
);
|
||||
router.use(
|
||||
'/profit_loss_sheet',
|
||||
Container.get(ProfitLossController).router()
|
||||
);
|
||||
router.use(
|
||||
'/general_ledger',
|
||||
Container.get(GeneralLedgerController).router()
|
||||
);
|
||||
router.use(
|
||||
'/trial_balance_sheet',
|
||||
Container.get(TrialBalanceSheetController).router()
|
||||
);
|
||||
router.use('/journal', Container.get(JournalSheetController).router());
|
||||
router.use(
|
||||
'/receivable_aging_summary',
|
||||
Container.get(ARAgingSummary).router()
|
||||
);
|
||||
router.use(
|
||||
'/payable_aging_summary',
|
||||
Container.get(APAgingSummary).router()
|
||||
);
|
||||
router.use(
|
||||
'/purchases-by-items',
|
||||
Container.get(PurchasesByItemsController).router()
|
||||
);
|
||||
router.use(
|
||||
'/sales-by-items',
|
||||
Container.get(SalesByItemsController).router()
|
||||
);
|
||||
router.use(
|
||||
'/inventory-valuation',
|
||||
Container.get(InventoryValuationController).router()
|
||||
);
|
||||
router.use(
|
||||
'/customer-balance-summary',
|
||||
Container.get(CustomerBalanceSummaryController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/vendor-balance-summary',
|
||||
Container.get(VendorBalanceSummaryController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-customers',
|
||||
Container.get(TransactionsByCustomers).router(),
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-vendors',
|
||||
Container.get(TransactionsByVendors).router(),
|
||||
);
|
||||
router.use(
|
||||
'/cash-flow',
|
||||
Container.get(CashFlowStatementController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/inventory-item-details',
|
||||
Container.get(InventoryDetailsController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-reference',
|
||||
Container.get(TransactionsByReferenceController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/cashflow-account-transactions',
|
||||
Container.get(CashflowAccountTransactions).router(),
|
||||
);
|
||||
router.use(
|
||||
'/project-profitability-summary',
|
||||
Container.get(ProjectProfitabilityController).router(),
|
||||
)
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import APAgingSummaryReportService from '@/services/FinancialStatements/AgingSummary/APAgingSummaryService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
export default class APAgingSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
APAgingSummaryService: APAgingSummaryReportService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_AP_AGING_SUMMARY, AbilitySubject.Report),
|
||||
this.validationSchema,
|
||||
asyncMiddleware(this.payableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').isInt({ min: 1 }).toInt(),
|
||||
query('none_zero').default(true).isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
*/
|
||||
async payableAgingSummary(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import ARAgingSummaryService from '@/services/FinancialStatements/AgingSummary/ARAgingSummaryService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class ARAgingSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
ARAgingSummaryService: ARAgingSummaryService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_AR_AGING_SUMMARY, AbilitySubject.Report),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.receivableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* AR aging summary validation roles.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
query('aging_days_before').optional().isInt({ max: 500 }).toInt(),
|
||||
query('aging_periods').optional().isInt({ max: 12 }).toInt(),
|
||||
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').isInt({ min: 1 }).toInt(),
|
||||
|
||||
query('none_zero').default(true).isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve AR aging summary report.
|
||||
*/
|
||||
async receivableAgingSummary(req: Request, res: Response) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { castArray } from 'lodash';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BalanceSheetStatementService from '@/services/FinancialStatements/BalanceSheet/BalanceSheetService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import BalanceSheetTable from '@/services/FinancialStatements/BalanceSheet/BalanceSheetTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class BalanceSheetStatementController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
balanceSheetService: BalanceSheetStatementService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_BALANCE_SHEET, AbilitySubject.Report),
|
||||
this.balanceSheetValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.balanceSheet.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get balanceSheetValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('accounting_method').optional().isIn(['cash', 'accural']),
|
||||
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
query('display_columns_type').optional().isIn(['date_periods', 'total']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
|
||||
query('account_ids').isArray().optional(),
|
||||
query('account_ids.*').isNumeric().toInt(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Percentage of column/row.
|
||||
query('percentage_of_column').optional().isBoolean().toBoolean(),
|
||||
query('percentage_of_row').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Camparsion periods periods.
|
||||
query('previous_period').optional().isBoolean().toBoolean(),
|
||||
query('previous_period_amount_change').optional().isBoolean().toBoolean(),
|
||||
query('previous_period_percentage_change')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
// Camparsion periods periods.
|
||||
query('previous_year').optional().isBoolean().toBoolean(),
|
||||
query('previous_year_amount_change').optional().isBoolean().toBoolean(),
|
||||
query('previous_year_percentage_change')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet.
|
||||
*/
|
||||
async balanceSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.balanceSheetService.balanceSheet(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
const table = new BalanceSheetTable(data, query, i18n);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res.status(200).send({
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
columns: table.tableColumns(),
|
||||
},
|
||||
query,
|
||||
meta,
|
||||
});
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send({ data, columns, query, meta });
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { query } from 'express-validator';
|
||||
import BaseController from "../BaseController";
|
||||
|
||||
export default class BaseFinancialReportController extends BaseController {
|
||||
|
||||
|
||||
get sheetNumberFormatValidationSchema() {
|
||||
return [
|
||||
query('number_format.precision')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: 5 })
|
||||
.toInt(),
|
||||
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
|
||||
query('number_format.show_zero').optional().isBoolean().toBoolean(),
|
||||
query('number_format.format_money')
|
||||
.optional()
|
||||
.isIn(['total', 'always', 'none'])
|
||||
.trim(),
|
||||
query('number_format.negative_format')
|
||||
.optional()
|
||||
.isIn(['parentheses', 'mines'])
|
||||
.trim()
|
||||
.escape(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { query } from 'express-validator';
|
||||
import {
|
||||
NextFunction,
|
||||
Router,
|
||||
Request,
|
||||
Response,
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import CashFlowStatementService from '@/services/FinancialStatements/CashFlow/CashFlowService';
|
||||
import {
|
||||
ICashFlowStatementDOO,
|
||||
ICashFlowStatement,
|
||||
AbilitySubject,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import CashFlowTable from '@/services/FinancialStatements/CashFlow/CashFlowTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class CashFlowController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
cashFlowService: CashFlowStatementService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_CASHFLOW, AbilitySubject.Report),
|
||||
this.cashflowValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.cashFlow.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get cashflowValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
query('display_columns_type').optional().isIn(['date_periods', 'total']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(cashFlowDOO: ICashFlowStatementDOO) {
|
||||
const { data, query, meta } = cashFlowDOO;
|
||||
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
cashFlowDOO: ICashFlowStatementDOO,
|
||||
tenantId: number
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const cashFlowTable = new CashFlowTable(cashFlowDOO, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: cashFlowTable.tableRows(),
|
||||
columns: cashFlowTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(cashFlowDOO.query),
|
||||
meta: this.transfromToResponse(cashFlowDOO.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async cashFlow(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = {
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const cashFlow = await this.cashFlowService.cashFlow(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(cashFlow, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send(this.transformJsonResponse(cashFlow));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { query } from 'express-validator';
|
||||
import {
|
||||
NextFunction,
|
||||
Router,
|
||||
Request,
|
||||
Response,
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ICashFlowStatementDOO,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import CashFlowTable from '@/services/FinancialStatements/CashFlow/CashFlowTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CashflowAccountTransactionsService from '@/services/FinancialStatements/CashflowAccountTransactions/CashflowAccountTransactionsService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class CashFlowAccountTransactionsController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
cashflowAccountTransactions: CashflowAccountTransactionsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_CASHFLOW_ACCOUNT_TRANSACTION,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.cashFlow),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cashflow account transactions validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('account_id').exists().isInt().toInt(),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(casahflowAccountTransactions) {
|
||||
const { transactions, pagination } = casahflowAccountTransactions;
|
||||
|
||||
return {
|
||||
transactions: this.transfromToResponse(transactions),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
cashFlowDOO: ICashFlowStatementDOO,
|
||||
tenantId: number
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const cashFlowTable = new CashFlowTable(cashFlowDOO, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: cashFlowTable.tableRows(),
|
||||
columns: cashFlowTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(cashFlowDOO.query),
|
||||
meta: this.transfromToResponse(cashFlowDOO.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private cashFlow = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const query = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const cashFlowAccountTransactions =
|
||||
await this.cashflowAccountTransactions.cashflowAccountTransactions(
|
||||
tenantId,
|
||||
query
|
||||
);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
// case 'application/json+table':
|
||||
// return res
|
||||
// .status(200)
|
||||
// .send(this.transformToTableRows(cashFlow, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformJsonResponse(cashFlowAccountTransactions));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param {Error} error - Error.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'ACCOUNT_ID_HAS_INVALID_TYPE') {
|
||||
return res.boom.badRequest(
|
||||
'The given account id should be cash, bank or credit card type.',
|
||||
{
|
||||
errors: [{ type: 'ACCOUNT_ID_HAS_INVALID_TYPE', code: 200 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'account_not_found') {
|
||||
return res.boom.notFound('The given account not found.', {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ICustomerBalanceSummaryStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import CustomerBalanceSummary from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import CustomerBalanceSummaryTableRows from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
export default class CustomerBalanceSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
customerBalanceSummaryService: CustomerBalanceSummary;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.customerBalanceSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
// As date.
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
// Percentages.
|
||||
query('percentage_column').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filters none-zero or none-transactions.
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Customers ids.
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the balance summary statement to table rows.
|
||||
* @param {ICustomerBalanceSummaryStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
tenantId,
|
||||
{ data, query }: ICustomerBalanceSummaryStatement
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const tableRows = new CustomerBalanceSummaryTableRows(data, query, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
columns: tableRows.tableColumns(),
|
||||
data: tableRows.tableRows(),
|
||||
},
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the balance summary statement to raw json.
|
||||
* @param {ICustomerBalanceSummaryStatement} customerBalance -
|
||||
*/
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
query,
|
||||
}: ICustomerBalanceSummaryStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async customerBalanceSummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const customerBalanceSummary =
|
||||
await this.customerBalanceSummaryService.customerBalanceSummary(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(tenantId, customerBalanceSummary));
|
||||
case 'application/json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToJsonResponse(customerBalanceSummary));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import GeneralLedgerService from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class GeneralLedgerReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
generalLedgetService: GeneralLedgerService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_GENERAL_LEDGET, AbilitySubject.Report),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.generalLedger.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('basis').optional(),
|
||||
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
|
||||
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||
|
||||
query('accounts_ids').optional().isArray({ min: 1 }),
|
||||
query('accounts_ids.*').isInt().toInt(),
|
||||
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async generalLedger(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.generalLedgetService.generalLedger(tenantId, filter);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { query } from 'express-validator';
|
||||
import {
|
||||
NextFunction,
|
||||
Router,
|
||||
Request,
|
||||
Response,
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import InventoryDetailsService from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsService';
|
||||
import InventoryDetailsTable from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class InventoryDetailsController extends BaseController {
|
||||
@Inject()
|
||||
inventoryDetailsService: InventoryDetailsService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_INVENTORY_ITEM_DETAILS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.inventoryDetails.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('number_format.precision')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: 5 })
|
||||
.toInt(),
|
||||
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
|
||||
query('number_format.negative_format')
|
||||
.optional()
|
||||
.isIn(['parentheses', 'mines'])
|
||||
.trim()
|
||||
.escape(),
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
|
||||
// Filtering by warehouses.
|
||||
query('warehouses_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('warehouses_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(inventoryDetails) {
|
||||
const { data, query, meta } = inventoryDetails;
|
||||
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
*/
|
||||
private transformToTableRows(inventoryDetails, tenantId: number) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const inventoryDetailsTable = new InventoryDetailsTable(
|
||||
inventoryDetails,
|
||||
i18n
|
||||
);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: inventoryDetailsTable.tableData(),
|
||||
columns: inventoryDetailsTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(inventoryDetails.query),
|
||||
meta: this.transfromToResponse(inventoryDetails.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async inventoryDetails(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = {
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const inventoryDetails =
|
||||
await this.inventoryDetailsService.inventoryDetails(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(inventoryDetails, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformJsonResponse(inventoryDetails));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import InventoryValuationService from '@/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class InventoryValuationReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
inventoryValuationService: InventoryValuationService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.inventoryValuation.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
|
||||
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||
query('none_zero').default(false).isBoolean().toBoolean(),
|
||||
query('only_active').default(false).isBoolean().toBoolean(),
|
||||
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
|
||||
// Filtering by warehouses.
|
||||
query('warehouses_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('warehouses_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async inventoryValuation(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.inventoryValuationService.inventoryValuationSheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { castArray } from 'lodash';
|
||||
import { query, oneOf } from 'express-validator';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import JournalSheetService from '@/services/FinancialStatements/JournalSheet/JournalSheetService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class JournalSheetController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
journalService: JournalSheetService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_JOURNAL, AbilitySubject.Report),
|
||||
this.journalValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.journal.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get journalValidationSchema() {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('transaction_type').optional().trim().escape(),
|
||||
query('transaction_id').optional().isInt().toInt(),
|
||||
oneOf(
|
||||
[
|
||||
query('account_ids').optional().isArray({ min: 1 }),
|
||||
query('account_ids.*').optional().isNumeric().toInt(),
|
||||
],
|
||||
[query('account_ids').optional().isNumeric().toInt()]
|
||||
),
|
||||
query('from_range').optional().isNumeric().toInt(),
|
||||
query('to_range').optional().isNumeric().toInt(),
|
||||
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the ledger report of the given account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async journal(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, query, meta } = await this.journalService.journalSheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import ProfitLossSheetService from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import { ProfitLossSheetTable } from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
@Service()
|
||||
export default class ProfitLossSheetController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
profitLossSheetService: ProfitLossSheetService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_PROFIT_LOSS, AbilitySubject.Report),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.profitLossSheet.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('basis').optional(),
|
||||
|
||||
query('from_date').optional().isISO8601().toDate(),
|
||||
query('to_date').optional().isISO8601().toDate(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
query('accounts_ids').isArray().optional(),
|
||||
query('accounts_ids.*').isNumeric().toInt(),
|
||||
|
||||
query('display_columns_type').optional().isIn(['total', 'date_periods']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
|
||||
// Percentage options.
|
||||
query('percentage_column').optional().isBoolean().toBoolean(),
|
||||
query('percentage_row').optional().isBoolean().toBoolean(),
|
||||
query('percentage_expense').optional().isBoolean().toBoolean(),
|
||||
query('percentage_income').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Camparsion periods periods.
|
||||
query('previous_period').optional().isBoolean().toBoolean(),
|
||||
query('previous_period_amount_change').optional().isBoolean().toBoolean(),
|
||||
query('previous_period_percentage_change')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
// Camparsion periods periods.
|
||||
query('previous_year').optional().isBoolean().toBoolean(),
|
||||
query('previous_year_amount_change').optional().isBoolean().toBoolean(),
|
||||
query('previous_year_percentage_change')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve profit/loss financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async profitLossSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.profitLossSheetService.profitLossSheet(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
const table = new ProfitLossSheetTable(data, query, i18n);
|
||||
|
||||
return res.status(200).send({
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
columns: table.tableColumns(),
|
||||
},
|
||||
query,
|
||||
meta,
|
||||
});
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send({
|
||||
data,
|
||||
query,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { query } from 'express-validator';
|
||||
import {
|
||||
NextFunction,
|
||||
Router,
|
||||
Request,
|
||||
Response,
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import {
|
||||
ICashFlowStatementDOO,
|
||||
AbilitySubject,
|
||||
ReportsAction,
|
||||
IProjectProfitabilitySummaryPOJO,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ProjectProfitabilitySummaryTable } from '@/services/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryTable';
|
||||
import { ProjectProfitabilitySummaryService } from '@/services/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryService';
|
||||
|
||||
@Service()
|
||||
export default class ProjectProfitabilityController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
private projectProfitabilityService: ProjectProfitabilitySummaryService;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_PROJECT_PROFITABILITY_SUMMARY,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.projectProfitabilitySummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by projects.
|
||||
query('products_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('products_ids.*').isNumeric().toInt(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(projectProfitabilityPOJO: IProjectProfitabilitySummaryPOJO) {
|
||||
const { data, query, meta } = projectProfitabilityPOJO;
|
||||
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
projectProfitabilityPOJO: IProjectProfitabilitySummaryPOJO,
|
||||
tenantId: number
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const projectProfitabilityTable = new ProjectProfitabilitySummaryTable(
|
||||
projectProfitabilityPOJO.data,
|
||||
i18n
|
||||
);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: projectProfitabilityTable.tableData(),
|
||||
columns: projectProfitabilityTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(projectProfitabilityPOJO.query),
|
||||
// meta: this.transfromToResponse(cashFlowDOO.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async projectProfitabilitySummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const projectProfitability =
|
||||
await this.projectProfitabilityService.projectProfitabilitySummary(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(projectProfitability, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformJsonResponse(projectProfitability));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import PurchasesByItemsService from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class PurchasesByItemReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
purchasesByItemsService: PurchasesByItemsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_PURCHASES_BY_ITEMS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.purchasesByItems.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
// Filter items.
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filters items.
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
query('only_active').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Specific items.
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.purchasesByItemsService.purchasesByItems(tenantId, filter);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import SalesByItemsReportService from '@/services/FinancialStatements/SalesByItems/SalesByItemsService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class SalesByItemsReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
salesByItemsService: SalesByItemsReportService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_SALES_BY_ITEMS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.purchasesByItems.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
// Specific items.
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
// Number format.
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filters items.
|
||||
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||
query('only_active').default(false).isBoolean().toBoolean(),
|
||||
|
||||
// Order by.
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } = await this.salesByItemsService.salesByItems(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ITransactionsByCustomersStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import TransactionsByCustomersService from '@/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService';
|
||||
import TransactionsByCustomersTableRows from '@/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class TransactionsByCustomersReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
transactionsByCustomersService: TransactionsByCustomersService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_CUSTOMERS_TRANSACTIONS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.transactionsByCustomers.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
private get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Customers ids.
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the statement to table rows response.
|
||||
* @param {ITransactionsByCustomersStatement} statement -
|
||||
*/
|
||||
private transformToTableResponse(customersTransactions, tenantId) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const table = new TransactionsByCustomersTableRows(
|
||||
customersTransactions,
|
||||
i18n
|
||||
);
|
||||
return {
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the statement to json response.
|
||||
* @param {ITransactionsByCustomersStatement} statement -
|
||||
*/
|
||||
private transfromToJsonResponse(
|
||||
data,
|
||||
columns
|
||||
): ITransactionsByCustomersStatement {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async transactionsByCustomers(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const report =
|
||||
await this.transactionsByCustomersService.transactionsByCustomers(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'json':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transfromToJsonResponse(report.data, report.columns));
|
||||
case 'application/json+table':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableResponse(report.data, tenantId));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import TransactionsByReferenceService from '@/services/FinancialStatements/TransactionsByReference';
|
||||
import { ITransactionsByReferenceTransaction } from '@/interfaces';
|
||||
@Service()
|
||||
export default class TransactionsByReferenceController extends BaseController {
|
||||
@Inject()
|
||||
private transactionsByReferenceService: TransactionsByReferenceService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.transactionsByReference.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('reference_id').exists().isInt(),
|
||||
query('reference_type').exists().isString(),
|
||||
|
||||
query('number_format.precision')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: 5 })
|
||||
.toInt(),
|
||||
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
|
||||
query('number_format.negative_format')
|
||||
.optional()
|
||||
.isIn(['parentheses', 'mines'])
|
||||
.trim()
|
||||
.escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions by the given reference type and id.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
public async transactionsByReference(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.transactionsByReferenceService.getTransactionsByReference(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToJsonResponse(data.transactions));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the given report transaction to json response.
|
||||
* @param transactions
|
||||
* @returns
|
||||
*/
|
||||
private transformToJsonResponse(
|
||||
transactions: ITransactionsByReferenceTransaction[]
|
||||
) {
|
||||
return {
|
||||
transactions: this.transfromToResponse(transactions),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import TransactionsByVendorsTableRows from '@/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows';
|
||||
import TransactionsByVendorsService from '@/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ITransactionsByVendorsStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
export default class TransactionsByVendorsReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
transactionsByVendorsService: TransactionsByVendorsService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_VENDORS_TRANSACTIONS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.transactionsByVendors.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Vendors ids.
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(tenantId: number, transactions: any[]) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const table = new TransactionsByVendorsTableRows(transactions, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: table.tableRows(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to json response.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
query,
|
||||
}: ITransactionsByVendorsStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async transactionsByVendors(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const report =
|
||||
await this.transactionsByVendorsService.transactionsByVendors(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(tenantId, report.data));
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send(this.transformToJsonResponse(report));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { castArray } from 'lodash';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import TrialBalanceSheetService from '@/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class TrialBalanceSheetController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
trialBalanceSheetService: TrialBalanceSheetService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_TRIAL_BALANCE_SHEET,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.trialBalanceSheetValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.trialBalanceSheet.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get trialBalanceSheetValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('basis').optional(),
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('account_ids').isArray().optional(),
|
||||
query('account_ids.*').isNumeric().toInt(),
|
||||
query('basis').optional(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
query('only_active').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the trial balance sheet.
|
||||
*/
|
||||
public async trialBalanceSheet(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, settings } = req;
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.trialBalanceSheetService.trialBalanceSheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import VendorBalanceSummaryTableRows from '@/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableRows';
|
||||
import VendorBalanceSummaryService from '@/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService';
|
||||
import {
|
||||
AbilitySubject,
|
||||
IVendorBalanceSummaryStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
export default class VendorBalanceSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
vendorBalanceSummaryService: VendorBalanceSummaryService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_VENDORS_SUMMARY_BALANCE,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
asyncMiddleware(this.vendorBalanceSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
// Percentage columns.
|
||||
query('percentage_column').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filters none-zero or none-transactions.
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Vendors ids.
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {IVendorBalanceSummaryStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
tenantId: number,
|
||||
{ data, query }: IVendorBalanceSummaryStatement
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const tableData = new VendorBalanceSummaryTableRows(
|
||||
data,
|
||||
query,
|
||||
i18n
|
||||
);
|
||||
return {
|
||||
table: {
|
||||
columns: tableData.tableColumns(),
|
||||
data: tableData.tableRows(),
|
||||
},
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to raw json.
|
||||
* @param {IVendorBalanceSummaryStatement} statement -
|
||||
*/
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
}: IVendorBalanceSummaryStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve vendors balance summary.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async vendorBalanceSummary(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const vendorBalanceSummary =
|
||||
await this.vendorBalanceSummaryService.vendorBalanceSummary(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(tenantId, vendorBalanceSummary));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToJsonResponse(vendorBalanceSummary));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import { InventoryCostApplication } from '@/services/Inventory/InventoryCostApplication';
|
||||
|
||||
@Service()
|
||||
export class InventoryItemsCostController extends BaseController {
|
||||
@Inject()
|
||||
private inventoryItemCost: InventoryCostApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/items-cost',
|
||||
[
|
||||
query('date').exists().isISO8601().toDate(),
|
||||
|
||||
query('items_ids').exists().isArray({ min: 1 }),
|
||||
query('items_ids.*').exists().isInt().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getItemsCosts)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given items costs.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getItemsCosts = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const itemsCostQueryDTO = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const costs = await this.inventoryItemCost.getItemsInventoryValuationList(
|
||||
tenantId,
|
||||
itemsCostQueryDTO.itemsIds,
|
||||
itemsCostQueryDTO.date
|
||||
);
|
||||
return res.status(200).send({ costs });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, query, param } from 'express-validator';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '../BaseController';
|
||||
import InventoryAdjustmentService from '@/services/Inventory/InventoryAdjustmentService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { AbilitySubject, InventoryAdjustmentAction } from '@/interfaces';
|
||||
import CheckPolicies from '../../middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class InventoryAdjustmentsController extends BaseController {
|
||||
@Inject()
|
||||
inventoryAdjustmentService: InventoryAdjustmentService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:id/publish',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.EDIT,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.publishInventoryAdjustment.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.DELETE,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteInventoryAdjustment.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/quick',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.CREATE,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
this.validatateQuickAdjustment,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.createQuickInventoryAdjustment.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.VIEW,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getInventoryAdjustment.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.VIEW,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
[...this.validateListQuerySchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getInventoryAdjustments.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate list query schema
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick inventory adjustment validation schema.
|
||||
*/
|
||||
get validatateQuickAdjustment() {
|
||||
return [
|
||||
check('date').exists().isISO8601(),
|
||||
check('type')
|
||||
.exists()
|
||||
.isIn(['increment', 'decrement', 'value_adjustment']),
|
||||
check('reference_no').exists(),
|
||||
check('adjustment_account_id').exists().isInt().toInt(),
|
||||
check('reason').exists().isString().exists(),
|
||||
check('description').optional().isString(),
|
||||
check('item_id').exists().isInt().toInt(),
|
||||
check('quantity')
|
||||
.if(check('type').exists().isIn(['increment', 'decrement']))
|
||||
.exists()
|
||||
.isInt()
|
||||
.toInt(),
|
||||
check('cost')
|
||||
.if(check('type').exists().isIn(['increment']))
|
||||
.exists()
|
||||
.isFloat()
|
||||
.toInt(),
|
||||
check('publish').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a quick inventory adjustment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async createQuickInventoryAdjustment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, user } = req;
|
||||
const quickInventoryAdjustment = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const inventoryAdjustment =
|
||||
await this.inventoryAdjustmentService.createQuickAdjustment(
|
||||
tenantId,
|
||||
quickInventoryAdjustment,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: inventoryAdjustment.id,
|
||||
message: 'The inventory adjustment has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given inventory adjustment transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteInventoryAdjustment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: adjustmentId } = req.params;
|
||||
|
||||
try {
|
||||
await this.inventoryAdjustmentService.deleteInventoryAdjustment(
|
||||
tenantId,
|
||||
adjustmentId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: adjustmentId,
|
||||
message: 'The inventory adjustment has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the given inventory adjustment transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async publishInventoryAdjustment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: adjustmentId } = req.params;
|
||||
|
||||
try {
|
||||
await this.inventoryAdjustmentService.publishInventoryAdjustment(
|
||||
tenantId,
|
||||
adjustmentId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: adjustmentId,
|
||||
message: 'The inventory adjustment has been published successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the specific inventory adjustment transaction of the given id.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async getInventoryAdjustment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: adjustmentId } = req.params;
|
||||
|
||||
try {
|
||||
const inventoryAdjustment =
|
||||
await this.inventoryAdjustmentService.getInventoryAdjustment(
|
||||
tenantId,
|
||||
adjustmentId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(inventoryAdjustment),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory adjustments paginated list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getInventoryAdjustments(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
columnSortBy: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
filterRoles: [],
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { pagination, inventoryAdjustments } =
|
||||
await this.inventoryAdjustmentService.getInventoryAdjustments(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
inventoy_adjustments: inventoryAdjustments,
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'INVENTORY_ADJUSTMENT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVENTORY_ADJUSTMENT.NOT.FOUND',
|
||||
code: 100,
|
||||
message: 'The inventory adjustment not found.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM.NOT.FOUND', code: 140 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_not_found') {
|
||||
return res.boom.notFound('The given account not found.', {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_SHOULD_BE_INVENTORY_TYPE') {
|
||||
return res.boom.badRequest(
|
||||
'You could not make adjustment on item has no inventory type.',
|
||||
{ errors: [{ type: 'ITEM_SHOULD_BE_INVENTORY_TYPE', code: 300 }] }
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [
|
||||
{ type: 'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED', code: 400 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4900,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
268
packages/server/src/api/controllers/InviteUsers.ts
Normal file
268
packages/server/src/api/controllers/InviteUsers.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, body, param } from 'express-validator';
|
||||
import { IInviteUserInput } from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from './BaseController';
|
||||
import InviteTenantUserService from '@/services/InviteUsers/TenantInviteUser';
|
||||
import AcceptInviteUserService from '@/services/InviteUsers/AcceptInviteUser';
|
||||
|
||||
@Service()
|
||||
export default class InviteUsersController extends BaseController {
|
||||
@Inject()
|
||||
inviteUsersService: InviteTenantUserService;
|
||||
|
||||
@Inject()
|
||||
acceptInviteService: AcceptInviteUserService;
|
||||
|
||||
/**
|
||||
* Routes that require authentication.
|
||||
*/
|
||||
authRouter() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/send',
|
||||
[
|
||||
body('email').exists().trim().escape(),
|
||||
body('role_id').exists().isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendInvite.bind(this)),
|
||||
this.handleServicesError
|
||||
);
|
||||
router.post(
|
||||
'/resend/:userId',
|
||||
[param('userId').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.resendInvite.bind(this)),
|
||||
this.handleServicesError
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes that non-required authentication.
|
||||
*/
|
||||
nonAuthRouter() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/accept/:token',
|
||||
[...this.inviteUserDTO],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.accept.bind(this)),
|
||||
this.handleServicesError
|
||||
);
|
||||
router.get(
|
||||
'/invited/:token',
|
||||
[param('token').exists().trim().escape()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.invited.bind(this)),
|
||||
this.handleServicesError
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite DTO schema validation.
|
||||
*/
|
||||
get inviteUserDTO() {
|
||||
return [
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
param('token').exists().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a user to the authorized user organization.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
async sendInvite(req: Request, res: Response, next: Function) {
|
||||
const sendInviteDTO = this.matchedBodyData(req);
|
||||
const { tenantId } = req;
|
||||
const { user } = req;
|
||||
|
||||
try {
|
||||
const { invite } = await this.inviteUsersService.sendInvite(
|
||||
tenantId,
|
||||
sendInviteDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'INVITE.SENT.SUCCESSFULLY',
|
||||
message: 'The invite has been sent to the given email.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend the user invite.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
async resendInvite(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { userId } = req.params;
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.resendInvite(tenantId, userId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'INVITE.RESEND.SUCCESSFULLY',
|
||||
message: 'The invite has been sent to the given email.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the inviation.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async accept(req: Request, res: Response, next: Function) {
|
||||
const inviteUserInput: IInviteUserInput = this.matchedBodyData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
});
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
await this.acceptInviteService.acceptInvite(token, inviteUserInput);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'USER.INVITE.ACCEPTED',
|
||||
message: 'User invite has been accepted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the invite token is valid.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async invited(req: Request, res: Response, next: Function) {
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
const { inviteToken, orgName } =
|
||||
await this.acceptInviteService.checkInvite(token);
|
||||
|
||||
return res.status(200).send({
|
||||
inviteToken: inviteToken.token,
|
||||
email: inviteToken.email,
|
||||
organizationName: orgName,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the service error.
|
||||
*/
|
||||
handleServicesError(error, req: Request, res: Response, next: Function) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'EMAIL_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'EMAIL.ALREADY.EXISTS',
|
||||
code: 100,
|
||||
message: 'Email already exists in the users.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EMAIL_ALREADY_INVITED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'EMAIL.ALREADY.INVITED',
|
||||
code: 200,
|
||||
message: 'Email already invited.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVITE_TOKEN_INVALID') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVITE.TOKEN.INVALID',
|
||||
code: 300,
|
||||
message: 'Invite token is invalid, please try another one.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PHONE_NUMBER_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'PHONE_NUMBER.EXISTS',
|
||||
code: 400,
|
||||
message:
|
||||
'Phone number is already invited, please try another unique one.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_RECENTLY_INVITED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'USER_RECENTLY_INVITED',
|
||||
code: 500,
|
||||
message:
|
||||
'This person was recently invited. No need to invite them again just yet.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ROLE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ROLE_NOT_FOUND',
|
||||
code: 600,
|
||||
message: 'The given user role is not found.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'USER_NOT_FOUND',
|
||||
code: 700,
|
||||
message: 'The given user is not found.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
311
packages/server/src/api/controllers/ItemCategories.ts
Normal file
311
packages/server/src/api/controllers/ItemCategories.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import ItemCategoriesService from '@/services/ItemCategories/ItemCategoriesService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { IItemCategoryOTD } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
|
||||
@Service()
|
||||
export default class ItemsCategoriesController extends BaseController {
|
||||
@Inject()
|
||||
itemCategoriesService: ItemCategoriesService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:id',
|
||||
[
|
||||
...this.categoryValidationSchema,
|
||||
...this.specificCategoryValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editCategory.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
[...this.categoryValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newCategory.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[...this.specificCategoryValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteItem.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[...this.specificCategoryValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getCategory.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
[...this.categoriesListValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getList.bind(this)),
|
||||
this.handlerServiceError,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item category validation schema.
|
||||
*/
|
||||
get categoryValidationSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ min: 0, max: DATATYPES_LENGTH.STRING }),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('sell_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('cost_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('inventory_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate items categories schema.
|
||||
*/
|
||||
get categoriesListValidationSchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().trim().escape().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate specific item category schema.
|
||||
*/
|
||||
get specificCategoryValidationSchema() {
|
||||
return [param('id').exists().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new item category.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async newCategory(req: Request, res: Response, next: NextFunction) {
|
||||
const { user, tenantId } = req;
|
||||
const itemCategoryOTD: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const itemCategory = await this.itemCategoriesService.newItemCategory(
|
||||
tenantId,
|
||||
itemCategoryOTD,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: itemCategory.id,
|
||||
message: 'The item category has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit details of the given category item.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async editCategory(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: itemCategoryId } = req.params;
|
||||
const itemCategoryOTD: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.itemCategoriesService.editItemCategory(
|
||||
tenantId,
|
||||
itemCategoryId,
|
||||
itemCategoryOTD,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: itemCategoryId,
|
||||
message: 'The item category has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the give item category.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: itemCategoryId } = req.params;
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
await this.itemCategoriesService.deleteItemCategory(
|
||||
tenantId,
|
||||
itemCategoryId,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: itemCategoryId,
|
||||
message: 'The item category has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of items.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async getList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
|
||||
const itemCategoriesFilter = {
|
||||
sortOrder: 'asc',
|
||||
columnSortBy: 'created_at',
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const {
|
||||
itemCategories,
|
||||
filterMeta,
|
||||
} = await this.itemCategoriesService.getItemCategoriesList(
|
||||
tenantId,
|
||||
itemCategoriesFilter,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
item_categories: itemCategories,
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details of the given category.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async getCategory(req: Request, res: Response, next: NextFunction) {
|
||||
const itemCategoryId: number = req.params.id;
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const itemCategory = await this.itemCategoriesService.getItemCategory(
|
||||
tenantId,
|
||||
itemCategoryId,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({ category: itemCategory });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service error.
|
||||
* @param {Error} error
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'CATEGORY_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_CATEGORY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_CATEGORIES_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_CATEGORIES_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CATEGORY_NAME_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CATEGORY_NAME_EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_COGS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_INVENTORY') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 900 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
522
packages/server/src/api/controllers/Items/Items.ts
Normal file
522
packages/server/src/api/controllers/Items/Items.ts
Normal file
@@ -0,0 +1,522 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { IItemDTO, ItemAction, AbilitySubject } from '@/interfaces';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckAbilities from '@/api/middleware/CheckPolicies';
|
||||
import { ItemsApplication } from '@/services/Items/ItemsApplication';
|
||||
|
||||
@Service()
|
||||
export default class ItemsController extends BaseController {
|
||||
@Inject()
|
||||
private itemsApplication: ItemsApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckAbilities(ItemAction.CREATE, AbilitySubject.Item),
|
||||
this.validateItemSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.newItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/activate',
|
||||
CheckAbilities(ItemAction.EDIT, AbilitySubject.Item),
|
||||
this.validateSpecificItemSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.activateItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/inactivate',
|
||||
CheckAbilities(ItemAction.EDIT, AbilitySubject.Item),
|
||||
[...this.validateSpecificItemSchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.inactivateItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckAbilities(ItemAction.EDIT, AbilitySubject.Item),
|
||||
[...this.validateItemSchema, ...this.validateSpecificItemSchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.editItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckAbilities(ItemAction.DELETE, AbilitySubject.Item),
|
||||
[...this.validateSpecificItemSchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckAbilities(ItemAction.VIEW, AbilitySubject.Item),
|
||||
[...this.validateSpecificItemSchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckAbilities(ItemAction.VIEW, AbilitySubject.Item),
|
||||
[...this.validateListQuerySchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getItemsList.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item schema.
|
||||
*/
|
||||
get validateItemSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('type')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isIn(['service', 'non-inventory', 'inventory']),
|
||||
check('code')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
// Purchase attributes.
|
||||
check('purchasable').optional().isBoolean().toBoolean(),
|
||||
check('cost_price')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toFloat()
|
||||
.if(check('purchasable').equals('true'))
|
||||
.exists(),
|
||||
check('cost_account_id').if(check('purchasable').equals('true')).exists(),
|
||||
check('cost_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
// Sell attributes.
|
||||
check('sellable').optional().isBoolean().toBoolean(),
|
||||
check('sell_price')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toFloat()
|
||||
.if(check('sellable').equals('true'))
|
||||
.exists(),
|
||||
check('sell_account_id').if(check('sellable').equals('true')).exists(),
|
||||
check('sell_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('inventory_account_id')
|
||||
.if(check('type').equals('inventory'))
|
||||
.exists(),
|
||||
check('inventory_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('sell_description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('purchase_description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('category_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('note')
|
||||
.optional()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('active').optional().isBoolean().toBoolean(),
|
||||
|
||||
check('media_ids').optional().isArray(),
|
||||
check('media_ids.*').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate specific item params schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get validateSpecificItemSchema(): ValidationChain[] {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate list query schema.
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate autocomplete list query schema.
|
||||
*/
|
||||
get autocompleteQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('limit').optional().isNumeric().toInt(),
|
||||
|
||||
query('keyword').optional().isString().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given item details to the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async newItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemDTO: IItemDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const storedItem = await this.itemsApplication.createItem(tenantId, itemDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
id: storedItem.id,
|
||||
message: 'The item has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given item details on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
const item: IItemDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.itemsApplication.editItem(tenantId, itemId, item);
|
||||
|
||||
return res.status(200).send({
|
||||
id: itemId,
|
||||
message: 'The item has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given item.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async activateItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
|
||||
try {
|
||||
await this.itemsApplication.activateItem(tenantId, itemId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: itemId,
|
||||
message: 'The item has been activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates the given item.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async inactivateItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
|
||||
try {
|
||||
await this.itemsApplication.inactivateItem(tenantId, itemId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: itemId,
|
||||
message: 'The item has been inactivated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given item from the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deleteItem(req: Request, res: Response, next: NextFunction) {
|
||||
const itemId: number = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.itemsApplication.deleteItem(tenantId, itemId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: itemId,
|
||||
message: 'The item has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details the given item id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getItem(req: Request, res: Response, next: NextFunction) {
|
||||
const itemId: number = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const item = await this.itemsApplication.getItem(tenantId, itemId);
|
||||
|
||||
return res.status(200).send({
|
||||
item: this.transfromToResponse(item),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve items datatable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getItemsList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const filter = {
|
||||
sortOrder: 'DESC',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
inactiveMode: false,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { items, pagination, filterMeta } =
|
||||
await this.itemsApplication.getItems(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
items: this.transfromToResponse(items),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM.NOT.FOUND', code: 140 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS_NOT_FOUND', code: 130 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_CATEOGRY_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM_CATEGORY.NOT.FOUND', code: 140 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_NAME_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM.NAME.ALREADY.EXISTS', code: 210 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_COGS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_COGS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_INVENTORY') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.INVENTORY.TYPE', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', code: 310 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', code: 330 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
|
||||
message: 'Cannot change inventory item type',
|
||||
code: 340,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
message:
|
||||
'Cannot change item type to inventory with item has associated transactions.',
|
||||
code: 350,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||
message:
|
||||
'Cannot change item inventory account while the item has transactions.',
|
||||
code: 360,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
||||
code: 370,
|
||||
message:
|
||||
'Could not delete item that has associated transactions.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
137
packages/server/src/api/controllers/Items/ItemsTransactions.ts
Normal file
137
packages/server/src/api/controllers/Items/ItemsTransactions.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import ItemTransactionsService from '@/services/Items/ItemTransactionsService';
|
||||
import BaseController from '../BaseController';
|
||||
|
||||
@Service()
|
||||
export default class ItemTransactionsController extends BaseController {
|
||||
@Inject()
|
||||
itemTransactionsService: ItemTransactionsService;
|
||||
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/:id/transactions/invoices',
|
||||
this.asyncMiddleware(this.getItemInvoicesTransactions)
|
||||
);
|
||||
router.get(
|
||||
'/:id/transactions/bills',
|
||||
this.asyncMiddleware(this.getItemBillTransactions)
|
||||
);
|
||||
router.get(
|
||||
'/:id/transactions/estimates',
|
||||
this.asyncMiddleware(this.getItemEstimateTransactions)
|
||||
);
|
||||
router.get(
|
||||
'/:id/transactions/receipts',
|
||||
this.asyncMiddleware(this.getItemReceiptTransactions)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve item associated invoices transactions.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
public getItemInvoicesTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.itemTransactionsService.getItemInvoicesTransactions(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve item associated bills transactions.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
public getItemBillTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: billId } = req.params;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.itemTransactionsService.getItemBillTransactions(
|
||||
tenantId,
|
||||
billId
|
||||
);
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve item associated estimates transactions.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
public getItemEstimateTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: estimateId } = req.params;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.itemTransactionsService.getItemEstimateTransactions(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
*/
|
||||
public getItemReceiptTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: receiptId } = req.params;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.itemTransactionsService.getItemReceiptTransactions(
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
17
packages/server/src/api/controllers/Items/index.ts
Normal file
17
packages/server/src/api/controllers/Items/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
import ItemsController from './Items';
|
||||
|
||||
import ItemTransactionsController from './ItemsTransactions';
|
||||
|
||||
@Service()
|
||||
export default class ItemsBaseController {
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/', Container.get(ItemsController).router());
|
||||
router.use('/', Container.get(ItemTransactionsController).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
60
packages/server/src/api/controllers/Jobs.ts
Normal file
60
packages/server/src/api/controllers/Jobs.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import JobsService from '@/services/Jobs/JobsService';
|
||||
|
||||
@Service()
|
||||
export default class ItemsController extends BaseController {
|
||||
@Inject()
|
||||
jobsService: JobsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/:id', this.getJob, this.handlerServiceErrors);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve job details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getJob = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
const job = await this.jobsService.getJob(id);
|
||||
|
||||
return res.status(200).send({
|
||||
job: this.transfromToResponse(job),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors = (
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
};
|
||||
}
|
||||
477
packages/server/src/api/controllers/ManualJournals.ts
Normal file
477
packages/server/src/api/controllers/ManualJournals.ts
Normal file
@@ -0,0 +1,477 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, ManualJournalAction } from '@/interfaces';
|
||||
import { ManualJournalsApplication } from '@/services/ManualJournals/ManualJournalsApplication';
|
||||
|
||||
@Service()
|
||||
export default class ManualJournalsController extends BaseController {
|
||||
@Inject()
|
||||
private manualJournalsApplication: ManualJournalsApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ManualJournalAction.View, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalsListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getManualJournalsList),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(ManualJournalAction.View, AbilitySubject.ManualJournal),
|
||||
asyncMiddleware(this.getManualJournal),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/publish',
|
||||
CheckPolicies(ManualJournalAction.Edit, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.publishManualJournal),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(ManualJournalAction.Edit, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalValidationSchema, ...this.manualJournalParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editManualJournal),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(ManualJournalAction.Delete, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteManualJournal),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(ManualJournalAction.Create, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.makeJournalEntries),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific manual journal id param validation schema.
|
||||
*/
|
||||
get manualJournalParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual journal DTO schema.
|
||||
*/
|
||||
get manualJournalValidationSchema() {
|
||||
return [
|
||||
check('date').exists().isISO8601(),
|
||||
check('currency_code').optional(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('journal_number')
|
||||
.optional()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('journal_type')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('reference')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
check('entries').isArray({ min: 2 }),
|
||||
check('entries.*.index')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('entries.*.credit')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toFloat(),
|
||||
check('entries.*.debit')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toFloat(),
|
||||
check('entries.*.account_id')
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('entries.*.note')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('entries.*.contact_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('entries.*.branch_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.project_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual journals list validation schema.
|
||||
*/
|
||||
get manualJournalsListSchema() {
|
||||
return [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Make manual journal.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private makeJournalEntries = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
const manualJournalDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { manualJournal } =
|
||||
await this.manualJournalsApplication.createManualJournal(
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: manualJournal.id,
|
||||
message: 'The manual journal has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edit the given manual journal.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private editManualJournal = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
const { id: manualJournalId } = req.params;
|
||||
const manualJournalDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { manualJournal } =
|
||||
await this.manualJournalsApplication.editManualJournal(
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
manualJournalDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: manualJournal.id,
|
||||
message: 'The manual journal has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the given manual journal details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getManualJournal = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: manualJournalId } = req.params;
|
||||
|
||||
try {
|
||||
const manualJournal =
|
||||
await this.manualJournalsApplication.getManualJournal(
|
||||
tenantId,
|
||||
manualJournalId
|
||||
);
|
||||
return res.status(200).send({
|
||||
manual_journal: this.transfromToResponse(manualJournal),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Publish the given manual journal.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private publishManualJournal = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: manualJournalId } = req.params;
|
||||
|
||||
try {
|
||||
await this.manualJournalsApplication.publishManualJournal(
|
||||
tenantId,
|
||||
manualJournalId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: manualJournalId,
|
||||
message: 'The manual journal has been published successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the given manual journal.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private deleteManualJournal = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
const { id: manualJournalId } = req.params;
|
||||
|
||||
try {
|
||||
await this.manualJournalsApplication.deleteManualJournal(
|
||||
tenantId,
|
||||
manualJournalId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: manualJournalId,
|
||||
message: 'Manual journal has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve manual journals list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
getManualJournalsList = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
try {
|
||||
const { manualJournals, pagination, filterMeta } =
|
||||
await this.manualJournalsApplication.getManualJournals(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
manual_journals: this.transfromToResponse(manualJournals),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches all service errors.
|
||||
* @param error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
catchServiceErrors = (
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'manual_journal_not_found') {
|
||||
res.boom.badRequest('Manual journal not found.', {
|
||||
errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'credit_debit_not_equal_zero') {
|
||||
return res.boom.badRequest(
|
||||
'Credit and debit should not be equal zero.',
|
||||
{
|
||||
errors: [
|
||||
{
|
||||
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
||||
code: 200,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'credit_debit_not_equal') {
|
||||
return res.boom.badRequest('Credit and debit should be equal.', {
|
||||
errors: [{ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'acccounts_ids_not_found') {
|
||||
return res.boom.badRequest(
|
||||
'Journal entries some of accounts ids not exists.',
|
||||
{ errors: [{ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 400 }] }
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'journal_number_exists') {
|
||||
return res.boom.badRequest('Journal number should be unique.', {
|
||||
errors: [{ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [
|
||||
{
|
||||
type: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
|
||||
code: 600,
|
||||
meta: this.transfromToResponse(error.payload),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CONTACTS_SHOULD_ASSIGN_WITH_VALID_ACCOUNT') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [
|
||||
{
|
||||
type: 'CONTACTS_SHOULD_ASSIGN_WITH_VALID_ACCOUNT',
|
||||
code: 700,
|
||||
meta: this.transfromToResponse(error.payload),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contacts_not_found') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [{ type: 'CONTACTS_NOT_FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MANUAL_JOURNAL_ALREADY_PUBLISHED') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [{ type: 'MANUAL_JOURNAL_ALREADY_PUBLISHED', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MANUAL_JOURNAL_NO_REQUIRED') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [
|
||||
{
|
||||
type: 'MANUAL_JOURNAL_NO_REQUIRED',
|
||||
message: 'The manual journal number required.',
|
||||
code: 1000,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS'
|
||||
) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS',
|
||||
code: 1100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID', code: 1200 },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
};
|
||||
}
|
||||
212
packages/server/src/api/controllers/Media.ts
Normal file
212
packages/server/src/api/controllers/Media.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import {
|
||||
param,
|
||||
query,
|
||||
check,
|
||||
} from 'express-validator';
|
||||
import { camelCase, upperFirst } from 'lodash';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IMediaLinkDTO } from '@/interfaces';
|
||||
import fs from 'fs';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from './BaseController';
|
||||
import MediaService from '@/services/Media/MediaService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
const fsPromises = fs.promises;
|
||||
|
||||
@Service()
|
||||
export default class MediaController extends BaseController {
|
||||
@Inject()
|
||||
mediaService: MediaService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post('/upload', [
|
||||
...this.uploadValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.uploadMedia.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.post('/:id/link', [
|
||||
...this.mediaIdParamSchema,
|
||||
...this.linkValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.linkMedia.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.delete('/', [
|
||||
...this.deleteValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteMedia.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.get('/:id', [
|
||||
...this.mediaIdParamSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getMedia.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
get uploadValidationSchema() {
|
||||
return [
|
||||
// check('attachment'),
|
||||
check('model_name').optional().trim().escape(),
|
||||
check('model_id').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
get linkValidationSchema() {
|
||||
return [
|
||||
check('model_name').exists().trim().escape(),
|
||||
check('model_id').exists().isNumeric().toInt(),
|
||||
]
|
||||
}
|
||||
|
||||
get deleteValidationSchema() {
|
||||
return [
|
||||
query('ids').exists().isArray(),
|
||||
query('ids.*').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
get mediaIdParamSchema() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all or the given attachment ids.
|
||||
* @param {Request} req -
|
||||
* @param {Response} req -
|
||||
* @param {NextFunction} req -
|
||||
*/
|
||||
async getMedia(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: mediaId } = req.params;
|
||||
|
||||
try {
|
||||
const media = await this.mediaService.getMedia(tenantId, mediaId);
|
||||
return res.status(200).send({ media });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads media.
|
||||
* @param {Request} req -
|
||||
* @param {Response} req -
|
||||
* @param {NextFunction} req -
|
||||
*/
|
||||
async uploadMedia(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { attachment } = req.files
|
||||
|
||||
const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
|
||||
const modelName = linkMediaDTO.modelName
|
||||
? upperFirst(camelCase(linkMediaDTO.modelName)) : '';
|
||||
|
||||
try {
|
||||
const media = await this.mediaService.upload(tenantId, attachment, modelName, linkMediaDTO.modelId);
|
||||
return res.status(200).send({ media_id: media.id });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given attachment ids from file system and database.
|
||||
* @param {Request} req -
|
||||
* @param {Response} req -
|
||||
* @param {NextFunction} req -
|
||||
*/
|
||||
async deleteMedia(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { ids: mediaIds } = req.query;
|
||||
|
||||
try {
|
||||
await this.mediaService.deleteMedia(tenantId, mediaIds);
|
||||
return res.status(200).send({
|
||||
media_ids: mediaIds
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given media to the specific resource model.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async linkMedia(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: mediaId } = req.params;
|
||||
const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
|
||||
const modelName = upperFirst(camelCase(linkMediaDTO.modelName));
|
||||
|
||||
try {
|
||||
await this.mediaService.linkMedia(tenantId, mediaId, linkMediaDTO.modelId, modelName);
|
||||
return res.status(200).send({ media_id: mediaId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerServiceErrors(error, req: Request, res: Response, next: NextFunction) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'MINETYPE_NOT_SUPPORTED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MINETYPE_NOT_SUPPORTED', code: 100, }]
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MEDIA_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MEDIA_NOT_FOUND', code: 200 }]
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MODEL_NAME_HAS_NO_MEDIA') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MODEL_NAME_HAS_NO_MEDIA', code: 300 }]
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MODEL_ID_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MODEL_ID_NOT_FOUND', code: 400 }]
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MEDIA_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MEDIA_IDS_NOT_FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MEDIA_LINK_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MEDIA_LINK_EXISTS', code: 600 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
41
packages/server/src/api/controllers/Miscellaneous/index.ts
Normal file
41
packages/server/src/api/controllers/Miscellaneous/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import MiscService from '@/services/Miscellaneous/MiscService';
|
||||
import DateFormatsService from '@/services/Miscellaneous/DateFormats';
|
||||
|
||||
@Service()
|
||||
export default class MiscController extends BaseController {
|
||||
@Inject()
|
||||
dateFormatsService: DateFormatsService;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/date_formats',
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.dateFormats.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve date formats options.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
dateFormats(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const dateFormats = this.dateFormatsService.getDateFormats();
|
||||
|
||||
return res.status(200).send({ data: dateFormats });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
199
packages/server/src/api/controllers/Organization.ts
Normal file
199
packages/server/src/api/controllers/Organization.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import moment from 'moment-timezone';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, ValidationChain } from 'express-validator';
|
||||
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import OrganizationService from '@/services/Organization/OrganizationService';
|
||||
import {
|
||||
ACCEPTED_CURRENCIES,
|
||||
MONTHS,
|
||||
ACCEPTED_LOCALES,
|
||||
} from '@/services/Organization/constants';
|
||||
import { DATE_FORMATS } from '@/services/Miscellaneous/DateFormats/constants';
|
||||
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
|
||||
const ACCEPTED_LOCATIONS = ['libya'];
|
||||
|
||||
@Service()
|
||||
export default class OrganizationController extends BaseController {
|
||||
@Inject()
|
||||
organizationService: OrganizationService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
// Should before build tenant database the user be authorized and
|
||||
// most important than that, should be subscribed to any plan.
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.use('/build', SubscriptionMiddleware('main'));
|
||||
router.post(
|
||||
'/build',
|
||||
this.organizationValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.build.bind(this)),
|
||||
this.handleServiceErrors.bind(this)
|
||||
);
|
||||
router.put(
|
||||
'/',
|
||||
this.organizationValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.updateOrganization.bind(this)),
|
||||
this.handleServiceErrors.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
asyncMiddleware(this.currentOrganization.bind(this)),
|
||||
this.handleServiceErrors.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization setup schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
private get organizationValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('name').exists().trim(),
|
||||
check('industry').optional().isString(),
|
||||
check('location').exists().isString().isIn(ACCEPTED_LOCATIONS),
|
||||
check('base_currency').exists().isIn(ACCEPTED_CURRENCIES),
|
||||
check('timezone').exists().isIn(moment.tz.names()),
|
||||
check('fiscal_year').exists().isIn(MONTHS),
|
||||
check('language').exists().isString().isIn(ACCEPTED_LOCALES),
|
||||
check('date_format').optional().isIn(DATE_FORMATS),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds tenant database and migrate database schema.
|
||||
* @param {Request} req - Express request.
|
||||
* @param {Response} res - Express response.
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async build(req: Request, res: Response, next: Function) {
|
||||
const { tenantId, user } = req;
|
||||
const buildDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const result = await this.organizationService.buildRunJob(
|
||||
tenantId,
|
||||
buildDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'ORGANIZATION.DATABASE.INITIALIZED',
|
||||
message: 'The organization database has been initialized.',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current organization of the associated authenticated user.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async currentOrganization(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const organization = await this.organizationService.currentOrganization(
|
||||
tenantId
|
||||
);
|
||||
return res.status(200).send({
|
||||
organization: this.transfromToResponse(organization),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the organization information.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
private async updateOrganization(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const tenantDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.organizationService.updateOrganization(tenantId, tenantDTO);
|
||||
|
||||
return res.status(200).send(
|
||||
this.transfromToResponse({
|
||||
tenantId,
|
||||
message: 'Organization information has been updated successfully.',
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'tenant_not_found') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TENANT_ALREADY_BUILT') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT_ALREADY_BUILT', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TENANT_IS_BUILDING') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT_IS_BUILDING', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BASE_CURRENCY_MUTATE_LOCKED') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BASE_CURRENCY_MUTATE_LOCKED', code: 400 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
124
packages/server/src/api/controllers/OrganizationDashboard.ts
Normal file
124
packages/server/src/api/controllers/OrganizationDashboard.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import OrganizationService from '../../services/Organization/OrganizationService';
|
||||
import OrganizationUpgrade from '../../services/Organization/OrganizationUpgrade';
|
||||
import { ServiceError } from '../../exceptions';
|
||||
|
||||
@Service()
|
||||
export default class OrganizationDashboardController extends BaseController {
|
||||
@Inject()
|
||||
organizationService: OrganizationService;
|
||||
|
||||
@Inject()
|
||||
organizationUpgrade: OrganizationUpgrade;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/base_currency_mutate',
|
||||
this.baseCurrencyMutateAbility.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/upgrade',
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.upgradeOrganization),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private async baseCurrencyMutateAbility(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: Function
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const abilities =
|
||||
await this.organizationService.mutateBaseCurrencyAbility(tenantId);
|
||||
|
||||
return res.status(200).send({ abilities });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade the authenticated organization.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
* @returns {Response}
|
||||
*/
|
||||
public upgradeOrganization = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
// Upgrade organization database.
|
||||
const { jobId } = await this.organizationUpgrade.upgrade(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
job_id: jobId,
|
||||
message: 'The organization has been upgraded successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
private handleServiceErrors = (
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'TENANT_DATABASE_UPGRADED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'TENANT_DATABASE_UPGRADED',
|
||||
message: 'Organization database is already upgraded.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TENANT_UPGRADE_IS_RUNNING') {
|
||||
return res.status(200).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'TENANT_UPGRADE_IS_RUNNING',
|
||||
message: 'Organization database upgrade is running.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
};
|
||||
}
|
||||
28
packages/server/src/api/controllers/Ping.ts
Normal file
28
packages/server/src/api/controllers/Ping.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
|
||||
export default class Ping {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.ping,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ping request.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async ping(req: Request, res: Response)
|
||||
{
|
||||
return res.status(200).send({
|
||||
server: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
273
packages/server/src/api/controllers/Projects/Projects.ts
Normal file
273
packages/server/src/api/controllers/Projects/Projects.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AbilitySubject, IProjectStatus, ProjectAction } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ProjectsApplication } from '@/services/Projects/Projects/ProjectsApplication';
|
||||
|
||||
@Service()
|
||||
export class ProjectsController extends BaseController {
|
||||
@Inject()
|
||||
private projectsApplication: ProjectsApplication;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(ProjectAction.CREATE, AbilitySubject.Project),
|
||||
[
|
||||
check('contact_id').exists(),
|
||||
check('name').exists().trim(),
|
||||
check('deadline').exists().isISO8601(),
|
||||
check('cost_estimate').exists().isDecimal(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.createProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(ProjectAction.EDIT, AbilitySubject.Project),
|
||||
[
|
||||
param('id').exists().isInt().toInt(),
|
||||
check('contact_id').exists(),
|
||||
check('name').exists().trim(),
|
||||
check('deadline').exists().isISO8601(),
|
||||
check('cost_estimate').exists().isDecimal(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.patch(
|
||||
'/:projectId/status',
|
||||
CheckPolicies(ProjectAction.EDIT, AbilitySubject.Project),
|
||||
[
|
||||
param('projectId').exists().isInt().toInt(),
|
||||
check('status')
|
||||
.exists()
|
||||
.isIn([IProjectStatus.InProgress, IProjectStatus.Closed]),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:projectId/billable/entries',
|
||||
CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project),
|
||||
[
|
||||
param('projectId').exists().isInt().toInt(),
|
||||
query('billable_type').optional().toArray(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.projectBillableEntries.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project),
|
||||
[],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getProjects.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(ProjectAction.DELETE, AbilitySubject.Project),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new project.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private async createProject(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const projectDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const account = await this.projectsApplication.createProject(
|
||||
tenantId,
|
||||
projectDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: account.id,
|
||||
message: 'The project has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit project details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
private async editProject(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { projectId } = req.params;
|
||||
|
||||
const editProjectDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const account = await this.projectsApplication.editProjectStatus(
|
||||
tenantId,
|
||||
projectId,
|
||||
editProjectDTO.status
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: account.id,
|
||||
message: 'The project has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of the given account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
private async getProject(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: projectId } = req.params;
|
||||
|
||||
try {
|
||||
const project = await this.projectsApplication.getProject(
|
||||
tenantId,
|
||||
projectId
|
||||
);
|
||||
return res.status(200).send({ project });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
private async deleteProject(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: accountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.projectsApplication.deleteProject(tenantId, accountId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: accountId,
|
||||
message: 'The deleted project has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts datatable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Response}
|
||||
*/
|
||||
private async getProjects(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
// Filter query.
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
inactiveMode: false,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const projects = await this.projectsApplication.getProjects(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
projects,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given billable entries of the given project.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private projectBillableEntries = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { projectId } = req.params;
|
||||
const query = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const billableEntries =
|
||||
await this.projectsApplication.getProjectBillableEntries(
|
||||
tenantId,
|
||||
projectId,
|
||||
query
|
||||
);
|
||||
return res.status(200).send({
|
||||
billableEntries,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms service errors to response.
|
||||
* @param {Error}
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
211
packages/server/src/api/controllers/Projects/Tasks.ts
Normal file
211
packages/server/src/api/controllers/Projects/Tasks.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AbilitySubject, AccountAction } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { TasksApplication } from '@/services/Projects/Tasks/TasksApplication';
|
||||
import { ProjectTaskChargeType } from '@/services/Projects/Tasks/constants';
|
||||
|
||||
@Service()
|
||||
export class ProjectTasksController extends BaseController {
|
||||
@Inject()
|
||||
private tasksApplication: TasksApplication;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/projects/:projectId/tasks',
|
||||
CheckPolicies(AccountAction.CREATE, AbilitySubject.Project),
|
||||
[
|
||||
check('name').exists(),
|
||||
check('charge_type')
|
||||
.exists()
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.isIn(Object.values(ProjectTaskChargeType)),
|
||||
check('rate').exists(),
|
||||
check('estimate_hours').exists(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.createTask.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/tasks/:taskId',
|
||||
CheckPolicies(AccountAction.EDIT, AbilitySubject.Project),
|
||||
[
|
||||
param('taskId').exists().isInt().toInt(),
|
||||
check('name').exists(),
|
||||
check('charge_type').exists().trim(),
|
||||
check('rate').exists(),
|
||||
check('estimate_hours').exists(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editTask.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/tasks/:taskId',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Project),
|
||||
[param('taskId').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getTask.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/projects/:projectId/tasks',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Project),
|
||||
[param('projectId').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getTasks.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/tasks/:taskId',
|
||||
CheckPolicies(AccountAction.DELETE, AbilitySubject.Project),
|
||||
[param('taskId').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteTask.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new project.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async createTask(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { projectId } = req.params;
|
||||
const taskDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const task = await this.tasksApplication.createTask(
|
||||
tenantId,
|
||||
projectId,
|
||||
taskDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: task.id,
|
||||
message: 'The task has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit project details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async editTask(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { taskId } = req.params;
|
||||
|
||||
const editTaskDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const task = await this.tasksApplication.editTask(
|
||||
tenantId,
|
||||
taskId,
|
||||
editTaskDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: task.id,
|
||||
message: 'The task has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of the given task.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getTask(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { taskId } = req.params;
|
||||
|
||||
try {
|
||||
const task = await this.tasksApplication.getTask(tenantId, taskId);
|
||||
|
||||
return res.status(200).send({ task });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given task.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteTask(req: Request, res: Response, next: NextFunction) {
|
||||
const { taskId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.tasksApplication.deleteTask(tenantId, taskId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: taskId,
|
||||
message: 'The deleted task has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts datatable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Response}
|
||||
*/
|
||||
public async getTasks(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { projectId } = req.params;
|
||||
|
||||
try {
|
||||
const tasks = await this.tasksApplication.getTasks(tenantId, projectId);
|
||||
|
||||
return res.status(200).send({ tasks });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms service errors to response.
|
||||
* @param {Error}
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
253
packages/server/src/api/controllers/Projects/Times.ts
Normal file
253
packages/server/src/api/controllers/Projects/Times.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AbilitySubject, AccountAction } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { TimesApplication } from '@/services/Projects/Times/TimesApplication';
|
||||
|
||||
@Service()
|
||||
export class ProjectTimesController extends BaseController {
|
||||
@Inject()
|
||||
private timesApplication: TimesApplication;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/projects/tasks/:taskId/times',
|
||||
CheckPolicies(AccountAction.CREATE, AbilitySubject.Project),
|
||||
[
|
||||
param('taskId').exists().isInt().toInt(),
|
||||
check('duration').exists().isDecimal(),
|
||||
check('description').exists().trim(),
|
||||
check('date').exists().isISO8601(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.createTime.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/projects/times/:timeId',
|
||||
CheckPolicies(AccountAction.EDIT, AbilitySubject.Project),
|
||||
[
|
||||
param('timeId').exists().isInt().toInt(),
|
||||
check('duration').exists().isDecimal(),
|
||||
check('description').exists().trim(),
|
||||
check('date').exists().isISO8601(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editTime.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/projects/times/:timeId',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Project),
|
||||
[
|
||||
param('timeId').exists().isInt().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getTime.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/projects/:projectId/times',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Project),
|
||||
[
|
||||
param('projectId').exists().isInt().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getTimeline.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/projects/times/:timeId',
|
||||
CheckPolicies(AccountAction.DELETE, AbilitySubject.Project),
|
||||
[
|
||||
param('timeId').exists().isInt().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteTime.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project create DTO Schema validation.
|
||||
*/
|
||||
get createTimeDTOSchema() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Project edit DTO Schema validation.
|
||||
*/
|
||||
get editProjectDTOSchema() {
|
||||
return [
|
||||
check('contact_id').exists(),
|
||||
check('name').exists().trim(),
|
||||
check('deadline').exists({ nullable: true }).isISO8601(),
|
||||
check('cost_estimate').exists().isDecimal(),
|
||||
];
|
||||
}
|
||||
|
||||
get accountParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts list validation schema.
|
||||
*/
|
||||
get accountsListSchema() {
|
||||
return [
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new project.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async createTime(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { taskId } = req.params;
|
||||
const taskDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const task = await this.timesApplication.createTime(
|
||||
tenantId,
|
||||
taskId,
|
||||
taskDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: task.id,
|
||||
message: 'The time entry has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit project details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async editTime(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { timeId } = req.params;
|
||||
|
||||
const editTaskDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const task = await this.timesApplication.editTime(
|
||||
tenantId,
|
||||
timeId,
|
||||
editTaskDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: task.id,
|
||||
message: 'The task has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of the given task.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getTime(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { timeId } = req.params;
|
||||
|
||||
try {
|
||||
const timeEntry = await this.timesApplication.getTime(tenantId, timeId);
|
||||
|
||||
return res.status(200).send({ timeEntry });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given task.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteTime(req: Request, res: Response, next: NextFunction) {
|
||||
const { timeId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.timesApplication.deleteTime(tenantId, timeId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: timeId,
|
||||
message: 'The deleted task has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts datatable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Response}
|
||||
*/
|
||||
public async getTimeline(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { projectId } = req.params;
|
||||
|
||||
try {
|
||||
const timeline = await this.timesApplication.getTimeline(
|
||||
tenantId,
|
||||
projectId
|
||||
);
|
||||
|
||||
return res.status(200).send({ timeline });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms service errors to response.
|
||||
* @param {Error}
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
547
packages/server/src/api/controllers/Purchases/Bills.ts
Normal file
547
packages/server/src/api/controllers/Purchases/Bills.ts
Normal file
@@ -0,0 +1,547 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { AbilitySubject, BillAction, IBillDTO, IBillEditDTO } from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BillsService from '@/services/Purchases/Bills';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import BillPaymentsService from '@/services/Purchases/BillPaymentsService';
|
||||
|
||||
@Service()
|
||||
export default class BillsController extends BaseController {
|
||||
@Inject()
|
||||
private billsService: BillsService;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
private billPayments: BillPaymentsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(BillAction.Create, AbilitySubject.Bill),
|
||||
[...this.billValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newBill.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id/open',
|
||||
CheckPolicies(BillAction.Edit, AbilitySubject.Bill),
|
||||
[...this.specificBillValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.openBill.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(BillAction.Edit, AbilitySubject.Bill),
|
||||
[...this.billEditValidationSchema, ...this.specificBillValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editBill.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/due',
|
||||
CheckPolicies(BillAction.View, AbilitySubject.Bill),
|
||||
[...this.dueBillsListingValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getDueBills.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(BillAction.View, AbilitySubject.Bill),
|
||||
[...this.specificBillValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getBill.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/:id/payment-transactions',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getBillPaymentsTransactions),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(BillAction.View, AbilitySubject.Bill),
|
||||
[...this.billsListingValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.billsList.bind(this)),
|
||||
this.handleServiceError,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(BillAction.Delete, AbilitySubject.Bill),
|
||||
[...this.specificBillValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteBill.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common validation schema.
|
||||
*/
|
||||
get billValidationSchema() {
|
||||
return [
|
||||
check('bill_number').exists().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('bill_date').exists().isISO8601(),
|
||||
check('due_date').optional().isISO8601(),
|
||||
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('note').optional().trim().escape(),
|
||||
check('open').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.landed_cost')
|
||||
.optional({ nullable: true })
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Common validation schema.
|
||||
*/
|
||||
get billEditValidationSchema() {
|
||||
return [
|
||||
check('bill_number').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('bill_date').exists().isISO8601(),
|
||||
check('due_date').optional().isISO8601(),
|
||||
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('note').optional().trim().escape(),
|
||||
check('open').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.id').optional().isNumeric().toInt(),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.landed_cost')
|
||||
.optional({ nullable: true })
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill validation schema.
|
||||
*/
|
||||
get specificBillValidationSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bills list validation schema.
|
||||
*/
|
||||
get billsListingValidationSchema() {
|
||||
return [
|
||||
query('view_slug').optional().isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
get dueBillsListingValidationSchema() {
|
||||
return [
|
||||
query('vendor_id').optional().trim().escape(),
|
||||
query('payment_made_id').optional().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new bill and records journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async newBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const billDTO: IBillDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const storedBill = await this.billsService.createBill(
|
||||
tenantId,
|
||||
billDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: storedBill.id,
|
||||
message: 'The bill has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit bill details with associated entries and rewrites journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: billId } = req.params;
|
||||
const { tenantId, user } = req;
|
||||
const billDTO: IBillEditDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.billsService.editBill(tenantId, billId, billDTO, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: billId,
|
||||
message: 'The bill has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given bill.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async openBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: billId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.billsService.openBill(tenantId, billId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: billId,
|
||||
message: 'The bill has been opened successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given bill details with associated item entries.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: billId } = req.params;
|
||||
|
||||
try {
|
||||
const bill = await this.billsService.getBill(tenantId, billId);
|
||||
|
||||
return res.status(200).send(this.transfromToResponse({ bill }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given bill with associated entries and journal transactions.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteBill(req: Request, res: Response, next: NextFunction) {
|
||||
const billId = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.billsService.deleteBill(tenantId, billId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: billId,
|
||||
message: 'The given bill deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing bills with pagination meta.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
public async billsList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { bills, pagination, filterMeta } =
|
||||
await this.billsService.getBills(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
bills: this.transfromToResponse(bills),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing all due bills of the given vendor.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public async getDueBills(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { vendorId } = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const bills = await this.billsService.getDueBills(tenantId, vendorId);
|
||||
return res.status(200).send({ bills });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payments transactions of specific bill.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getBillPaymentsTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: billId } = req.params;
|
||||
|
||||
try {
|
||||
const billPayments = await this.billPayments.getBillPayments(
|
||||
tenantId,
|
||||
billId
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: billPayments,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'BILL_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_NUMBER_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.NUMBER.EXISTS', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_VENDOR_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL_VENDOR_NOT_FOUND', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_ITEMS_NOT_PURCHASABLE') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL_ITEMS_NOT_PURCHASABLE', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_PURCHASE_ABLE_ITEMS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'NOT_PURCHASE_ABLE_ITEMS', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_ITEMS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_ENTRIES_IDS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL_ENTRIES_IDS_NOT_FOUND', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS_NOT_FOUND', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_ALREADY_OPEN') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'BILL_ALREADY_OPEN', code: 1100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'VENDOR_NOT_FOUND',
|
||||
message: 'Vendor not found.',
|
||||
code: 1200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES',
|
||||
message:
|
||||
'Cannot delete bill that has associated payment transactions.',
|
||||
code: 1200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_HAS_ASSOCIATED_LANDED_COSTS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'BILL_HAS_ASSOCIATED_LANDED_COSTS',
|
||||
message:
|
||||
'Cannot delete bill that has associated landed cost transactions.',
|
||||
code: 1300,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED',
|
||||
code: 1400,
|
||||
message:
|
||||
'Bill entries that have landed cost type can not be deleted.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES'
|
||||
) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES',
|
||||
code: 1500,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS',
|
||||
message:
|
||||
'Landed cost entries should be only with inventory items.',
|
||||
code: 1600,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_HAS_APPLIED_TO_VENDOR_CREDIT') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL_HAS_APPLIED_TO_VENDOR_CREDIT', code: 1700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
455
packages/server/src/api/controllers/Purchases/BillsPayments.ts
Normal file
455
packages/server/src/api/controllers/Purchases/BillsPayments.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import BillPaymentsService from '@/services/Purchases/BillPayments/BillPayments';
|
||||
import BillPaymentsPages from '@/services/Purchases/BillPayments/BillPaymentsPages';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, IPaymentMadeAction } from '@/interfaces';
|
||||
|
||||
/**
|
||||
* Bills payments controller.
|
||||
* @service
|
||||
*/
|
||||
@Service()
|
||||
export default class BillsPayments extends BaseController {
|
||||
@Inject()
|
||||
billPaymentService: BillPaymentsService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
billPaymentsPages: BillPaymentsPages;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(IPaymentMadeAction.Create, AbilitySubject.PaymentMade),
|
||||
[...this.billPaymentSchemaValidation],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.createBillPayment.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(IPaymentMadeAction.Edit, AbilitySubject.PaymentMade),
|
||||
[
|
||||
...this.billPaymentSchemaValidation,
|
||||
...this.specificBillPaymentValidateSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editBillPayment.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(IPaymentMadeAction.Delete, AbilitySubject.PaymentMade),
|
||||
[...this.specificBillPaymentValidateSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteBillPayment.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/new-page/entries',
|
||||
CheckPolicies(IPaymentMadeAction.View, AbilitySubject.PaymentMade),
|
||||
[query('vendor_id').exists()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getBillPaymentNewPageEntries.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/:id/edit-page',
|
||||
CheckPolicies(IPaymentMadeAction.View, AbilitySubject.PaymentMade),
|
||||
this.specificBillPaymentValidateSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getBillPaymentEditPage.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/:id/bills',
|
||||
CheckPolicies(IPaymentMadeAction.View, AbilitySubject.PaymentMade),
|
||||
this.specificBillPaymentValidateSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getPaymentBills.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(IPaymentMadeAction.View, AbilitySubject.PaymentMade),
|
||||
this.specificBillPaymentValidateSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getBillPayment.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(IPaymentMadeAction.View, AbilitySubject.PaymentMade),
|
||||
this.listingValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getBillsPayments.bind(this)),
|
||||
this.handleServiceError,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill payments schema validation.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get billPaymentSchemaValidation(): ValidationChain[] {
|
||||
return [
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('payment_number').optional({ nullable: true }).trim().escape(),
|
||||
check('payment_date').exists(),
|
||||
check('statement').optional().trim().escape(),
|
||||
check('reference').optional().trim().escape(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.index').optional().isNumeric().toInt(),
|
||||
check('entries.*.bill_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.payment_amount').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific bill payment schema validation.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get specificBillPaymentValidateSchema(): ValidationChain[] {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bills payment list validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get listingValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve bill payment new page entries.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async getBillPaymentNewPageEntries(req: Request, res: Response) {
|
||||
const { tenantId } = req;
|
||||
const { vendorId } = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const entries = await this.billPaymentsPages.getNewPageEntries(
|
||||
tenantId,
|
||||
vendorId
|
||||
);
|
||||
return res.status(200).send({
|
||||
entries: this.transfromToResponse(entries),
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the bill payment edit page details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getBillPaymentEditPage(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
try {
|
||||
const { billPayment, entries } =
|
||||
await this.billPaymentsPages.getBillPaymentEditPage(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
bill_payment: this.transfromToResponse(billPayment),
|
||||
entries: this.transfromToResponse(entries),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bill payment.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Response} res
|
||||
*/
|
||||
async createBillPayment(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const billPaymentDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const billPayment = await this.billPaymentService.createBillPayment(
|
||||
tenantId,
|
||||
billPaymentDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: billPayment.id,
|
||||
message: 'Payment made has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given bill payment details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editBillPayment(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const billPaymentDTO = this.matchedBodyData(req);
|
||||
const { id: billPaymentId } = req.params;
|
||||
|
||||
try {
|
||||
const paymentMade = await this.billPaymentService.editBillPayment(
|
||||
tenantId,
|
||||
billPaymentId,
|
||||
billPaymentDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: paymentMade.id,
|
||||
message: 'Payment made has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the bill payment and revert the journal
|
||||
* transactions with accounts balance.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response} res -
|
||||
*/
|
||||
async deleteBillPayment(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: billPaymentId } = req.params;
|
||||
|
||||
try {
|
||||
await this.billPaymentService.deleteBillPayment(tenantId, billPaymentId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: billPaymentId,
|
||||
message: 'Payment made has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the bill payment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getBillPayment(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: billPaymentId } = req.params;
|
||||
|
||||
try {
|
||||
const billPayment = await this.billPaymentService.getBillPayment(
|
||||
tenantId,
|
||||
billPaymentId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
bill_payment: this.transfromToResponse(billPayment),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve associated bills for the given payment made.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getPaymentBills(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: billPaymentId } = req.params;
|
||||
|
||||
try {
|
||||
const bills = await this.billPaymentService.getPaymentBills(
|
||||
tenantId,
|
||||
billPaymentId
|
||||
);
|
||||
return res.status(200).send({ bills });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve bills payments listing with pagination metadata.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async getBillsPayments(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const billPaymentsFilter = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
filterRoles: [],
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { billPayments, pagination, filterMeta } =
|
||||
await this.billPaymentService.listBillPayments(
|
||||
tenantId,
|
||||
billPaymentsFilter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
bill_payments: this.transfromToResponse(billPayments),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'PAYMENT_MADE_NOT_FOUND') {
|
||||
return res.status(404).send({
|
||||
message: 'Payment made not found.',
|
||||
errors: [{ type: 'BILL_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.PAYMENT.VENDOR.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'PAYMENT_ACCOUNT.NOT.CURRENT_ASSET.TYPE', code: 300 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_PAYMENT_NUMBER_NOT_UNQIUE') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === '') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILLS.IDS.NOT.EXISTS', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_PAYMENT_ENTRIES_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVALID_BILL_PAYMENT_AMOUNT') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVALID_BILL_PAYMENT_AMOUNT', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_ENTRIES_IDS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILLS_NOT_FOUND', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY', code: 1100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILLS_NOT_OPENED_YET') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'BILLS_NOT_OPENED_YET',
|
||||
message: 'The given bills are not opened yet.',
|
||||
code: 1200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'WITHDRAWAL_ACCOUNT_CURRENCY_INVALID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'WITHDRAWAL_ACCOUNT_CURRENCY_INVALID', code: 1300 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
305
packages/server/src/api/controllers/Purchases/LandedCost.ts
Normal file
305
packages/server/src/api/controllers/Purchases/LandedCost.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BillAllocatedCostTransactions from '@/services/Purchases/LandedCost/BillAllocatedLandedCostTransactions';
|
||||
import BaseController from '../BaseController';
|
||||
import AllocateLandedCost from '@/services/Purchases/LandedCost/AllocateLandedCost';
|
||||
import RevertAllocatedLandedCost from '@/services/Purchases/LandedCost/RevertAllocatedLandedCost';
|
||||
import LandedCostTranasctions from '@/services/Purchases/LandedCost/LandedCostTransactions';
|
||||
|
||||
@Service()
|
||||
export default class BillAllocateLandedCost extends BaseController {
|
||||
@Inject()
|
||||
allocateLandedCost: AllocateLandedCost;
|
||||
|
||||
@Inject()
|
||||
billAllocatedCostTransactions: BillAllocatedCostTransactions;
|
||||
|
||||
@Inject()
|
||||
revertAllocatedLandedCost: RevertAllocatedLandedCost;
|
||||
|
||||
@Inject()
|
||||
landedCostTranasctions: LandedCostTranasctions;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/bills/:billId/allocate',
|
||||
[
|
||||
check('transaction_id').exists().isInt(),
|
||||
check('transaction_type').exists().isIn(['Expense', 'Bill']),
|
||||
check('transaction_entry_id').exists().isInt(),
|
||||
|
||||
check('allocation_method').exists().isIn(['value', 'quantity']),
|
||||
check('description').optional({ nullable: true }),
|
||||
|
||||
check('items').isArray({ min: 1 }),
|
||||
check('items.*.entry_id').isInt(),
|
||||
check('items.*.cost').isDecimal(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.calculateLandedCost,
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:allocatedLandedCostId',
|
||||
[param('allocatedLandedCostId').exists().isInt()],
|
||||
this.validationResult,
|
||||
this.deleteAllocatedLandedCost,
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/transactions',
|
||||
[query('transaction_type').exists().isIn(['Expense', 'Bill'])],
|
||||
this.validationResult,
|
||||
this.getLandedCostTransactions,
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/bills/:billId/transactions',
|
||||
[param('billId').exists()],
|
||||
this.validationResult,
|
||||
this.getBillLandedCostTransactions,
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the landed cost transactions of the given query.
|
||||
* @param {Request} req - Request
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
private getLandedCostTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const query = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.landedCostTranasctions.getLandedCostTransactions(
|
||||
tenantId,
|
||||
query
|
||||
);
|
||||
|
||||
return res.status(200).send({ transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Allocate landed cost.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public calculateLandedCost = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { billId: purchaseInvoiceId } = req.params;
|
||||
const landedCostDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const billLandedCost = await this.allocateLandedCost.allocateLandedCost(
|
||||
tenantId,
|
||||
landedCostDTO,
|
||||
purchaseInvoiceId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: billLandedCost.id,
|
||||
message: 'The items cost are located successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the allocated landed cost.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public deleteAllocatedLandedCost = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response> => {
|
||||
const { tenantId } = req;
|
||||
const { allocatedLandedCostId } = req.params;
|
||||
|
||||
try {
|
||||
await this.revertAllocatedLandedCost.deleteAllocatedLandedCost(
|
||||
tenantId,
|
||||
allocatedLandedCostId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: allocatedLandedCostId,
|
||||
message: 'The allocated landed cost are delete successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the list unlocated landed costs.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public listLandedCosts = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const query = this.matchedQueryData(req);
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.landedCostTranasctions.getLandedCostTransactions(
|
||||
tenantId,
|
||||
query
|
||||
);
|
||||
|
||||
return res.status(200).send({ transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the bill landed cost transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getBillLandedCostTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response> => {
|
||||
const { tenantId } = req;
|
||||
const { billId } = req.params;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.billAllocatedCostTransactions.getBillLandedCostTransactions(
|
||||
tenantId,
|
||||
billId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
billId,
|
||||
transactions: this.transfromToResponse(transactions),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @param {Error} error
|
||||
*/
|
||||
public handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'BILL_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'BILL_NOT_FOUND',
|
||||
message: 'The give bill id not found.',
|
||||
code: 100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LANDED_COST_TRANSACTION_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LANDED_COST_TRANSACTION_NOT_FOUND',
|
||||
message: 'The given landed cost transaction id not found.',
|
||||
code: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LANDED_COST_ENTRY_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LANDED_COST_ENTRY_NOT_FOUND',
|
||||
message: 'The given landed cost tranasction entry id not found.',
|
||||
code: 300,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT',
|
||||
code: 400,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LANDED_COST_ITEMS_IDS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LANDED_COST_ITEMS_IDS_NOT_FOUND',
|
||||
message: 'The given entries ids of purchase invoice not found.',
|
||||
code: 500,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_LANDED_COST_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'BILL_LANDED_COST_NOT_FOUND',
|
||||
message: 'The given bill located landed cost not found.',
|
||||
code: 600,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_TRASNACTION_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COST_TRASNACTION_NOT_FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
660
packages/server/src/api/controllers/Purchases/VendorCredit.ts
Normal file
660
packages/server/src/api/controllers/Purchases/VendorCredit.ts
Normal file
@@ -0,0 +1,660 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
IVendorCreditCreateDTO,
|
||||
IVendorCreditEditDTO,
|
||||
VendorCreditAction,
|
||||
} from '@/interfaces';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import CreateVendorCredit from '@/services/Purchases/VendorCredits/CreateVendorCredit';
|
||||
import EditVendorCredit from '@/services/Purchases/VendorCredits/EditVendorCredit';
|
||||
import DeleteVendorCredit from '@/services/Purchases/VendorCredits/DeleteVendorCredit';
|
||||
import GetVendorCredit from '@/services/Purchases/VendorCredits/GetVendorCredit';
|
||||
import ListVendorCredits from '@/services/Purchases/VendorCredits/ListVendorCredits';
|
||||
import CreateRefundVendorCredit from '@/services/Purchases/VendorCredits/RefundVendorCredits/CreateRefundVendorCredit';
|
||||
import DeleteRefundVendorCredit from '@/services/Purchases/VendorCredits/RefundVendorCredits/DeleteRefundVendorCredit';
|
||||
import ListVendorCreditRefunds from '@/services/Purchases/VendorCredits/RefundVendorCredits/ListRefundVendorCredits';
|
||||
import OpenVendorCredit from '@/services/Purchases/VendorCredits/OpenVendorCredit';
|
||||
import GetRefundVendorCredit from '@/services/Purchases/VendorCredits/RefundVendorCredits/GetRefundVendorCredit';
|
||||
|
||||
@Service()
|
||||
export default class VendorCreditController extends BaseController {
|
||||
@Inject()
|
||||
createVendorCreditService: CreateVendorCredit;
|
||||
|
||||
@Inject()
|
||||
editVendorCreditService: EditVendorCredit;
|
||||
|
||||
@Inject()
|
||||
deleteVendorCreditService: DeleteVendorCredit;
|
||||
|
||||
@Inject()
|
||||
getVendorCreditService: GetVendorCredit;
|
||||
|
||||
@Inject()
|
||||
listCreditNotesService: ListVendorCredits;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
createRefundCredit: CreateRefundVendorCredit;
|
||||
|
||||
@Inject()
|
||||
deleteRefundCredit: DeleteRefundVendorCredit;
|
||||
|
||||
@Inject()
|
||||
listRefundCredit: ListVendorCreditRefunds;
|
||||
|
||||
@Inject()
|
||||
openVendorCreditService: OpenVendorCredit;
|
||||
|
||||
@Inject()
|
||||
getRefundCredit: GetRefundVendorCredit;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(VendorCreditAction.Create, AbilitySubject.VendorCredit),
|
||||
this.vendorCreditCreateDTOSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.newVendorCredit),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(VendorCreditAction.Edit, AbilitySubject.VendorCredit),
|
||||
this.vendorCreditEditDTOSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.editVendorCredit),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(VendorCreditAction.View, AbilitySubject.VendorCredit),
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getVendorCredit),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(VendorCreditAction.View, AbilitySubject.VendorCredit),
|
||||
this.billsListingValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getVendorCreditsList),
|
||||
this.handleServiceError,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(VendorCreditAction.Delete, AbilitySubject.VendorCredit),
|
||||
this.deleteDTOValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteVendorCredit),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id/open',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.openVendorCreditTransaction),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/:id/refund',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.vendorCreditRefundTransactions),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id/refund',
|
||||
CheckPolicies(VendorCreditAction.Refund, AbilitySubject.VendorCredit),
|
||||
this.vendorCreditRefundValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.refundVendorCredit),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.get(
|
||||
'/refunds/:refundId',
|
||||
this.getRefundCreditTransactionSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getRefundCreditTransaction),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.delete(
|
||||
'/refunds/:refundId',
|
||||
CheckPolicies(VendorCreditAction.Refund, AbilitySubject.VendorCredit),
|
||||
this.deleteRefundVendorCreditSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteRefundVendorCredit),
|
||||
this.handleServiceError
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common validation schema.
|
||||
*/
|
||||
get vendorCreditCreateDTOSchema() {
|
||||
return [
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('vendor_credit_number')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('vendor_credit_date').exists().isISO8601().toDate(),
|
||||
check('note').optional().trim().escape(),
|
||||
check('open').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Common validation schema.
|
||||
*/
|
||||
get vendorCreditEditDTOSchema() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('vendor_credit_number')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('vendor_credit_date').exists().isISO8601().toDate(),
|
||||
check('note').optional().trim().escape(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.id').optional().isNumeric().toInt(),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bills list validation schema.
|
||||
*/
|
||||
get billsListingValidationSchema() {
|
||||
return [
|
||||
query('view_slug').optional().isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
get deleteDTOValidationSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
get getRefundCreditTransactionSchema() {
|
||||
return [param('refundId').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
get deleteRefundVendorCreditSchema() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refund vendor credit validation schema.
|
||||
*/
|
||||
get vendorCreditRefundValidationSchema() {
|
||||
return [
|
||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||
check('description').exists(),
|
||||
|
||||
check('amount').exists().isNumeric().toFloat(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('reference_no').optional(),
|
||||
check('date').exists().isISO8601().toDate(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new bill and records journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private newVendorCredit = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
const vendorCreditCreateDTO: IVendorCreditCreateDTO =
|
||||
this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const vendorCredit = await this.createVendorCreditService.newVendorCredit(
|
||||
tenantId,
|
||||
vendorCreditCreateDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: vendorCredit.id,
|
||||
message: 'The vendor credit has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edit bill details with associated entries and rewrites journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
private editVendorCredit = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { id: billId } = req.params;
|
||||
const { tenantId, user } = req;
|
||||
const vendorCreditEditDTO: IVendorCreditEditDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.editVendorCreditService.editVendorCredit(
|
||||
tenantId,
|
||||
billId,
|
||||
vendorCreditEditDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: billId,
|
||||
message: 'The vendor credit has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the given bill details with associated item entries.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
private getVendorCredit = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: billId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.getVendorCreditService.getVendorCredit(
|
||||
tenantId,
|
||||
billId
|
||||
);
|
||||
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given bill with associated entries and journal transactions.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
private deleteVendorCredit = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const vendorCreditId = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.deleteVendorCreditService.deleteVendorCredit(
|
||||
tenantId,
|
||||
vendorCreditId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: vendorCreditId,
|
||||
message: 'The given vendor credit has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve vendor credits list.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private getVendorCreditsList = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { vendorCredits, pagination, filterMeta } =
|
||||
await this.listCreditNotesService.getVendorCredits(tenantId, filter);
|
||||
|
||||
return res.status(200).send({ vendorCredits, pagination, filterMeta });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Refunds vendor credit.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
private refundVendorCredit = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const refundDTO = this.matchedBodyData(req);
|
||||
const { id: vendorCreditId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const refundVendorCredit = await this.createRefundCredit.createRefund(
|
||||
tenantId,
|
||||
vendorCreditId,
|
||||
refundDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: refundVendorCredit.id,
|
||||
message: 'The vendor credit refund has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes refund vendor credit transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private deleteRefundVendorCredit = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { refundId: vendorCreditId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.deleteRefundCredit.deleteRefundVendorCreditRefund(
|
||||
tenantId,
|
||||
vendorCreditId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: vendorCreditId,
|
||||
message: 'The vendor credit refund has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve refunds transactions associated to vendor credit transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private vendorCreditRefundTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { id: vendorCreditId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const transactions = await this.listRefundCredit.getVendorCreditRefunds(
|
||||
tenantId,
|
||||
vendorCreditId
|
||||
);
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open vendor credit transaction.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
private openVendorCreditTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { id: vendorCreditId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.openVendorCreditService.openVendorCredit(
|
||||
tenantId,
|
||||
vendorCreditId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: vendorCreditId,
|
||||
message: 'The vendor credit has been opened successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private getRefundCreditTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { refundId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const refundCredit =
|
||||
await this.getRefundCredit.getRefundCreditTransaction(
|
||||
tenantId,
|
||||
refundId
|
||||
);
|
||||
return res.status(200).send({ refundCredit });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'ENTRIES_ITEMS_IDS_NOT_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ENTRIES_ITEMS_IDS_NOT_EXISTS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDOR_NOT_FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_CREDIT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDOR_CREDIT_NOT_FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'DEPOSIT_ACCOUNT_INVALID_TYPE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'DEPOSIT_ACCOUNT_INVALID_TYPE', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'REFUND_VENDOR_CREDIT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'REFUND_VENDOR_CREDIT_NOT_FOUND', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_CREDIT_HAS_NO_CREDITS_REMAINING') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'VENDOR_CREDIT_HAS_NO_CREDITS_REMAINING', code: 800 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_CREDIT_ALREADY_OPENED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDOR_CREDIT_ALREADY_OPENED', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_CREDIT_HAS_APPLIED_BILLS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDOR_CREDIT_HAS_APPLIED_BILLS', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS', code: 1200 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param, check } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import ApplyVendorCreditToBills from '@/services/Purchases/VendorCredits/ApplyVendorCreditToBills/ApplyVendorCreditToBills';
|
||||
import DeleteApplyVendorCreditToBill from '@/services/Purchases/VendorCredits/ApplyVendorCreditToBills/DeleteApplyVendorCreditToBill';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import GetAppliedBillsToVendorCredit from '@/services/Purchases/VendorCredits/ApplyVendorCreditToBills/GetAppliedBillsToVendorCredit';
|
||||
import GetVendorCreditToApplyBills from '@/services/Purchases/VendorCredits/ApplyVendorCreditToBills/GetVendorCreditToApplyBills';
|
||||
|
||||
@Service()
|
||||
export default class VendorCreditApplyToBills extends BaseController {
|
||||
@Inject()
|
||||
applyVendorCreditToBillsService: ApplyVendorCreditToBills;
|
||||
|
||||
@Inject()
|
||||
deleteAppliedCreditToBillsService: DeleteApplyVendorCreditToBill;
|
||||
|
||||
@Inject()
|
||||
getAppliedBillsToCreditService: GetAppliedBillsToVendorCredit;
|
||||
|
||||
@Inject()
|
||||
getCreditToApplyBillsService: GetVendorCreditToApplyBills;
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:id/apply-to-bills',
|
||||
[
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
check('entries.*.bill_id').exists().isInt().toInt(),
|
||||
check('entries.*.amount').exists().isNumeric().toFloat(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.applyVendorCreditToBills),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/applied-to-bills/:applyId',
|
||||
[param('applyId').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteApplyCreditToBill),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/apply-to-bills',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getVendorCreditAssociatedBillsToApply),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/applied-bills',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getVendorCreditAppliedBills),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply vendor credit to the given bills.
|
||||
* @param {Request}
|
||||
* @param {Response}
|
||||
* @param {NextFunction}
|
||||
*/
|
||||
public applyVendorCreditToBills = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: vendorCreditId } = req.params;
|
||||
const applyCreditToBillsDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.applyVendorCreditToBillsService.applyVendorCreditToBills(
|
||||
tenantId,
|
||||
vendorCreditId,
|
||||
applyCreditToBillsDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: vendorCreditId,
|
||||
message:
|
||||
'The vendor credit has been applied to the given bills successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes vendor credit applied to bill transaction.
|
||||
* @param {Request}
|
||||
* @param {Response}
|
||||
* @param {NextFunction}
|
||||
*/
|
||||
public deleteApplyCreditToBill = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { applyId } = req.params;
|
||||
|
||||
try {
|
||||
await this.deleteAppliedCreditToBillsService.deleteApplyVendorCreditToBills(
|
||||
tenantId,
|
||||
applyId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: applyId,
|
||||
message:
|
||||
'The applied vendor credit to bill has been deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public getVendorCreditAssociatedBillsToApply = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: vendorCreditId } = req.params;
|
||||
|
||||
try {
|
||||
const bills =
|
||||
await this.getCreditToApplyBillsService.getCreditToApplyBills(
|
||||
tenantId,
|
||||
vendorCreditId
|
||||
);
|
||||
return res.status(200).send({ data: bills });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public getVendorCreditAppliedBills = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: vendorCreditId } = req.params;
|
||||
|
||||
try {
|
||||
const appliedBills =
|
||||
await this.getAppliedBillsToCreditService.getAppliedBills(
|
||||
tenantId,
|
||||
vendorCreditId
|
||||
);
|
||||
return res.status(200).send({ data: appliedBills });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param next
|
||||
*/
|
||||
handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'VENDOR_CREDIT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDOR_CREDIT_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_ENTRIES_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'BILL_ENTRIES_IDS_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILLS_NOT_OPENED_YET') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'BILLS_NOT_OPENED_YET', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILLS_HAS_NO_REMAINING_AMOUNT') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'BILLS_HAS_NO_REMAINING_AMOUNT', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT', code: 500 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND', code: 600 },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
25
packages/server/src/api/controllers/Purchases/index.ts
Normal file
25
packages/server/src/api/controllers/Purchases/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Router } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
import Bills from '@/api/controllers/Purchases/Bills';
|
||||
import BillPayments from '@/api/controllers/Purchases/BillsPayments';
|
||||
import BillAllocateLandedCost from './LandedCost';
|
||||
import VendorCredit from './VendorCredit';
|
||||
import VendorCreditApplyToBills from './VendorCreditApplyToBills';
|
||||
|
||||
@Service()
|
||||
export default class PurchasesController {
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/bills', Container.get(Bills).router());
|
||||
router.use('/bill_payments', Container.get(BillPayments).router());
|
||||
router.use('/landed-cost', Container.get(BillAllocateLandedCost).router());
|
||||
router.use('/vendor-credit', Container.get(VendorCredit).router());
|
||||
router.use(
|
||||
'/vendor-credit',
|
||||
Container.get(VendorCreditApplyToBills).router()
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
82
packages/server/src/api/controllers/Resources.ts
Normal file
82
packages/server/src/api/controllers/Resources.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param } from 'express-validator';
|
||||
import BaseController from './BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import ResourceService from '@/services/Resource/ResourceService';
|
||||
|
||||
@Service()
|
||||
export default class ResourceController extends BaseController {
|
||||
@Inject()
|
||||
resourcesService: ResourceService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/:resource_model/meta',
|
||||
[
|
||||
param('resource_model').exists().trim().escape()
|
||||
],
|
||||
this.asyncMiddleware(this.resourceMeta.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve resource model meta.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
* @returns {Response}
|
||||
*/
|
||||
public resourceMeta = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Response => {
|
||||
const { tenantId } = req;
|
||||
const { resource_model: resourceModel } = req.params;
|
||||
|
||||
try {
|
||||
const resourceMeta = this.resourcesService.getResourceMeta(
|
||||
tenantId,
|
||||
resourceModel
|
||||
);
|
||||
return res.status(200).send({
|
||||
resource_meta: this.transfromToResponse(
|
||||
resourceMeta,
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'RESOURCE.MODEL.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import RolePermissionsSchema from '@/services/Roles/RolePermissionsSchema';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import BaseController from '../BaseController';
|
||||
|
||||
@Service()
|
||||
export default class RolePermissionsSchemaController extends BaseController {
|
||||
@Inject()
|
||||
rolePermissionSchema: RolePermissionsSchema;
|
||||
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/permissions/schema', this.getPermissionsSchema);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the role permissions schema.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getPermissionsSchema = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const permissionsSchema =
|
||||
this.rolePermissionSchema.getRolePermissionsSchema(tenantId);
|
||||
|
||||
return res.status(200).send({ data: permissionsSchema });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
254
packages/server/src/api/controllers/Roles/Roles.ts
Normal file
254
packages/server/src/api/controllers/Roles/Roles.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import RolesService from '@/services/Roles/RolesService';
|
||||
|
||||
@Service()
|
||||
export default class RolesController extends BaseController {
|
||||
@Inject()
|
||||
rolesService: RolesService;
|
||||
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
[
|
||||
check('role_name').exists().trim(),
|
||||
check('role_description').optional(),
|
||||
check('permissions').exists().isArray({ min: 1 }),
|
||||
check('permissions.*.subject').exists().trim(),
|
||||
check('permissions.*.ability').exists().trim(),
|
||||
check('permissions.*.value').exists().isBoolean().toBoolean(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.createRole),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[
|
||||
check('role_name').exists().trim(),
|
||||
check('role_description').optional(),
|
||||
check('permissions').isArray({ min: 1 }),
|
||||
check('permissions.*.permission_id'),
|
||||
check('permissions.*.subject').exists().trim(),
|
||||
check('permissions.*.ability').exists().trim(),
|
||||
check('permissions.*.value').exists().isBoolean().toBoolean(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.editRole),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteRole),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getRole),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.listRoles),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new role on the authenticated tenant.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
*/
|
||||
private createRole = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const newRoleDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const role = await this.rolesService.createRole(tenantId, newRoleDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
data: { roleId: role.id },
|
||||
message: 'The role has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given role from the storage.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private deleteRole = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: roleId } = req.params;
|
||||
|
||||
try {
|
||||
const role = await this.rolesService.deleteRole(tenantId, roleId);
|
||||
|
||||
return res.status(200).send({
|
||||
data: { roleId },
|
||||
message: 'The given role has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits the given role details on the storage.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private editRole = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: roleId } = req.params;
|
||||
const editRoleDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const role = await this.rolesService.editRole(tenantId, roleId, editRoleDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
data: { roleId },
|
||||
message: 'The given role hsa been updated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the roles list.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private listRoles = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const roles = await this.rolesService.listRoles(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
roles,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the specific role details.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private getRole = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { tenantId } = req;
|
||||
const { id: roleId } = req.params;
|
||||
|
||||
try {
|
||||
const role = await this.rolesService.getRole(tenantId, roleId);
|
||||
|
||||
return res.status(200).send({
|
||||
role,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the service errors.
|
||||
* @param error
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private handleServiceErrors = (
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'ROLE_PREFINED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ROLE_PREFINED',
|
||||
message: 'Role is predefined, you cannot modify predefined roles',
|
||||
code: 100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ROLE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ROLE_NOT_FOUND',
|
||||
message: 'Role is not found',
|
||||
code: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVALIDATE_PERMISSIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVALIDATE_PERMISSIONS',
|
||||
message: 'The submit role has invalid permissions.',
|
||||
code: 300,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CANNT_DELETE_ROLE_ASSOCIATED_TO_USERS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'CANNOT_DELETE_ROLE_ASSOCIATED_TO_USERS',
|
||||
message: 'Cannot delete role associated to users.',
|
||||
code: 400
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
22
packages/server/src/api/controllers/Roles/index.ts
Normal file
22
packages/server/src/api/controllers/Roles/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
|
||||
import BaseController from '../BaseController';
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
|
||||
import RolesService from '@/services/Roles/RolesService';
|
||||
import PermissionsSchema from './PermissionsSchema';
|
||||
import RolesController from './Roles';
|
||||
@Service()
|
||||
export default class RolesBaseController extends BaseController {
|
||||
@Inject()
|
||||
rolesService: RolesService;
|
||||
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/', Container.get(PermissionsSchema).router());
|
||||
router.use('/', Container.get(RolesController).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
846
packages/server/src/api/controllers/Sales/CreditNotes.ts
Normal file
846
packages/server/src/api/controllers/Sales/CreditNotes.ts
Normal file
@@ -0,0 +1,846 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
CreditNoteAction,
|
||||
ICreditNoteEditDTO,
|
||||
ICreditNoteNewDTO,
|
||||
} from '@/interfaces';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import CreateCreditNote from '@/services/CreditNotes/CreateCreditNote';
|
||||
import EditCreditNote from '@/services/CreditNotes/EditCreditNote';
|
||||
import DeleteCreditNote from '@/services/CreditNotes/DeleteCreditNote';
|
||||
import GetCreditNote from '@/services/CreditNotes/GetCreditNote';
|
||||
import ListCreditNotes from '@/services/CreditNotes/ListCreditNotes';
|
||||
import DeleteRefundCreditNote from '@/services/CreditNotes/DeleteRefundCreditNote';
|
||||
import ListCreditNoteRefunds from '@/services/CreditNotes/ListCreditNoteRefunds';
|
||||
import OpenCreditNote from '@/services/CreditNotes/OpenCreditNote';
|
||||
import CreateRefundCreditNote from '@/services/CreditNotes/CreateRefundCreditNote';
|
||||
import CreditNoteApplyToInvoices from '@/services/CreditNotes/CreditNoteApplyToInvoices';
|
||||
import DeletreCreditNoteApplyToInvoices from '@/services/CreditNotes/DeleteCreditNoteApplyToInvoices';
|
||||
import GetCreditNoteAssociatedInvoicesToApply from '@/services/CreditNotes/GetCreditNoteAssociatedInvoicesToApply';
|
||||
import GetCreditNoteAssociatedAppliedInvoices from '@/services/CreditNotes/GetCreditNoteAssociatedAppliedInvoices';
|
||||
import GetRefundCreditTransaction from '@/services/CreditNotes/GetRefundCreditNoteTransaction';
|
||||
import GetCreditNotePdf from '../../../services/CreditNotes/GetCreditNotePdf';
|
||||
/**
|
||||
* Credit notes controller.
|
||||
* @service
|
||||
*/
|
||||
@Service()
|
||||
export default class PaymentReceivesController extends BaseController {
|
||||
@Inject()
|
||||
createCreditNoteService: CreateCreditNote;
|
||||
|
||||
@Inject()
|
||||
editCreditNoteService: EditCreditNote;
|
||||
|
||||
@Inject()
|
||||
deleteCreditNoteService: DeleteCreditNote;
|
||||
|
||||
@Inject()
|
||||
getCreditNoteService: GetCreditNote;
|
||||
|
||||
@Inject()
|
||||
listCreditNotesService: ListCreditNotes;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
createCreditNoteRefund: CreateRefundCreditNote;
|
||||
|
||||
@Inject()
|
||||
deleteRefundCredit: DeleteRefundCreditNote;
|
||||
|
||||
@Inject()
|
||||
listCreditRefunds: ListCreditNoteRefunds;
|
||||
|
||||
@Inject()
|
||||
openCreditNote: OpenCreditNote;
|
||||
|
||||
@Inject()
|
||||
applyCreditNoteToInvoicesService: CreditNoteApplyToInvoices;
|
||||
|
||||
@Inject()
|
||||
deleteApplyCreditToInvoicesService: DeletreCreditNoteApplyToInvoices;
|
||||
|
||||
@Inject()
|
||||
getCreditAssociatedInvoicesToApply: GetCreditNoteAssociatedInvoicesToApply;
|
||||
|
||||
@Inject()
|
||||
getCreditAssociatedAppliedInvoices: GetCreditNoteAssociatedAppliedInvoices;
|
||||
|
||||
@Inject()
|
||||
getRefundCreditService: GetRefundCreditTransaction;
|
||||
|
||||
@Inject()
|
||||
creditNotePdf: GetCreditNotePdf;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
// Edit credit note.
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(CreditNoteAction.Edit, AbilitySubject.CreditNote),
|
||||
this.editCreditNoteDTOShema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.editCreditNote),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
// New credit note.
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(CreditNoteAction.Create, AbilitySubject.CreditNote),
|
||||
[...this.newCreditNoteDTOSchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.newCreditNote),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
// Get specific credit note.
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(CreditNoteAction.View, AbilitySubject.CreditNote),
|
||||
this.getCreditNoteSchema,
|
||||
this.asyncMiddleware(this.getCreditNote),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
// Get credit note list.
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(CreditNoteAction.View, AbilitySubject.CreditNote),
|
||||
this.validatePaymentReceiveList,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getCreditNotesList),
|
||||
this.handleServiceErrors,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
// Get specific credit note.
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(CreditNoteAction.Delete, AbilitySubject.CreditNote),
|
||||
this.deleteCreditNoteSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteCreditNote),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/open',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.openCreditNoteTransaction),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/refund',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.creditNoteRefundTransactions),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/refund',
|
||||
CheckPolicies(CreditNoteAction.Refund, AbilitySubject.CreditNote),
|
||||
this.creditNoteRefundSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.refundCreditNote),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/apply-to-invoices',
|
||||
this.creditNoteApplyToInvoices,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.applyCreditNoteToInvoices),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/refunds/:refundId',
|
||||
this.deleteRefundCreditSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteCreditNoteRefund),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/refunds/:refundId',
|
||||
this.getRefundCreditTransactionSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getRefundCreditTransaction),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/applied-to-invoices/:applyId',
|
||||
[param('applyId').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteApplyCreditToInvoices),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/apply-to-invoices',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getCreditNoteInvoicesToApply),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/applied-invoices',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getCreditNoteAppliedInvoices),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment receive schema.
|
||||
* @return {Array}
|
||||
*/
|
||||
get creditNoteDTOSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('credit_note_date').exists().isISO8601().toDate(),
|
||||
check('reference_no').optional(),
|
||||
check('credit_note_number').optional({ nullable: true }).trim().escape(),
|
||||
check('note').optional().trim().escape(),
|
||||
check('terms_conditions').optional().trim().escape(),
|
||||
check('open').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment receive list validation schema.
|
||||
*/
|
||||
get validatePaymentReceiveList(): ValidationChain[] {
|
||||
return [
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate payment receive parameters.
|
||||
*/
|
||||
get deleteCreditNoteSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* New credit note DTO validation schema.
|
||||
* @return {Array}
|
||||
*/
|
||||
get newCreditNoteDTOSchema() {
|
||||
return [...this.creditNoteDTOSchema];
|
||||
}
|
||||
|
||||
/**
|
||||
* Geet credit note validation schema.
|
||||
*/
|
||||
get getCreditNoteSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit credit note DTO validation schema.
|
||||
*/
|
||||
get editCreditNoteDTOShema() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
...this.creditNoteDTOSchema,
|
||||
];
|
||||
}
|
||||
|
||||
get creditNoteRefundSchema() {
|
||||
return [
|
||||
check('from_account_id').exists().isNumeric().toInt(),
|
||||
check('description').optional(),
|
||||
|
||||
check('amount').exists().isNumeric().toFloat(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('reference_no').optional(),
|
||||
check('date').exists().isISO8601().toDate(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
get creditNoteApplyToInvoices() {
|
||||
return [
|
||||
check('entries').isArray({ min: 1 }),
|
||||
check('entries.*.invoice_id').exists().isInt().toInt(),
|
||||
check('entries.*.amount').exists().isNumeric().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
get deleteRefundCreditSchema() {
|
||||
return [check('refundId').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
get getRefundCreditTransactionSchema() {
|
||||
return [check('refundId').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Records payment receive to the given customer with associated invoices.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
private newCreditNote = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
const creditNoteDTO: ICreditNoteNewDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const creditNote = await this.createCreditNoteService.newCreditNote(
|
||||
tenantId,
|
||||
creditNoteDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: creditNote.id,
|
||||
message: 'The credit note has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edit the given payment receive.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
private editCreditNote = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: creditNoteId } = req.params;
|
||||
|
||||
const creditNoteDTO: ICreditNoteEditDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.editCreditNoteService.editCreditNote(
|
||||
tenantId,
|
||||
creditNoteId,
|
||||
creditNoteDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: creditNoteId,
|
||||
message: 'The credit note has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delets the given payment receive id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
private deleteCreditNote = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
const { id: creditNoteId } = req.params;
|
||||
|
||||
try {
|
||||
await this.deleteCreditNoteService.deleteCreditNote(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: creditNoteId,
|
||||
message: 'The credit note has been deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve payment receive list with pagination metadata.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
private getCreditNotesList = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { creditNotes, pagination, filterMeta } =
|
||||
await this.listCreditNotesService.getCreditNotesList(tenantId, filter);
|
||||
|
||||
return res.status(200).send({ creditNotes, pagination, filterMeta });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the payment receive details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getCreditNote = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: creditNoteId } = req.params;
|
||||
|
||||
try {
|
||||
const creditNote = await this.getCreditNoteService.getCreditNote(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
);
|
||||
const ACCEPT_TYPE = {
|
||||
APPLICATION_PDF: 'application/pdf',
|
||||
APPLICATION_JSON: 'application/json',
|
||||
};
|
||||
// Response formatter.
|
||||
res.format({
|
||||
// Json content type.
|
||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
||||
return res
|
||||
.status(200)
|
||||
.send({ credit_note: this.transfromToResponse(creditNote) });
|
||||
},
|
||||
// Pdf content type.
|
||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
||||
const pdfContent = await this.creditNotePdf.getCreditNotePdf(
|
||||
tenantId,
|
||||
creditNote
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Refunds the credit note.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
private refundCreditNote = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: creditNoteId } = req.params;
|
||||
const creditNoteRefundDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const creditNoteRefund =
|
||||
await this.createCreditNoteRefund.createCreditNoteRefund(
|
||||
tenantId,
|
||||
creditNoteId,
|
||||
creditNoteRefundDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: creditNoteRefund.id,
|
||||
message:
|
||||
'The customer credit note refund has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply credit note to the given invoices.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private applyCreditNoteToInvoices = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: creditNoteId } = req.params;
|
||||
const applyCreditNoteToInvoicesDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.applyCreditNoteToInvoicesService.applyCreditNoteToInvoices(
|
||||
tenantId,
|
||||
creditNoteId,
|
||||
applyCreditNoteToInvoicesDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: creditNoteId,
|
||||
message:
|
||||
'The credit note has been applied the given invoices successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the credit note refund transaction.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private deleteCreditNoteRefund = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { refundId: creditRefundId } = req.params;
|
||||
|
||||
try {
|
||||
await this.deleteRefundCredit.deleteCreditNoteRefund(
|
||||
tenantId,
|
||||
creditRefundId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: creditRefundId,
|
||||
message: 'The credit note refund has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve get refund credit note transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
private getRefundCreditTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { refundId: creditRefundId } = req.params;
|
||||
|
||||
try {
|
||||
const refundCredit =
|
||||
await this.getRefundCreditService.getRefundCreditTransaction(
|
||||
tenantId,
|
||||
creditRefundId
|
||||
);
|
||||
return res.status(200).send({ refundCredit });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve refund transactions associated to the given credit note.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private creditNoteRefundTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { id: creditNoteId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const transactions = await this.listCreditRefunds.getCreditNoteRefunds(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
);
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private openCreditNoteTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { id: creditNoteId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const creditNote = await this.openCreditNote.openCreditNote(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'The credit note has been opened successfully',
|
||||
id: creditNote.id,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private deleteApplyCreditToInvoices = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { applyId: creditAppliedToInvoicesId } = req.params;
|
||||
|
||||
try {
|
||||
await this.deleteApplyCreditToInvoicesService.deleteApplyCreditNoteToInvoices(
|
||||
tenantId,
|
||||
creditAppliedToInvoicesId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: creditAppliedToInvoicesId,
|
||||
message:
|
||||
'The applied credit to invoices has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the credit note associated invoices to apply.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
*/
|
||||
private getCreditNoteInvoicesToApply = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: creditNoteId } = req.params;
|
||||
|
||||
try {
|
||||
const saleInvoices =
|
||||
await this.getCreditAssociatedInvoicesToApply.getCreditAssociatedInvoicesToApply(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
);
|
||||
return res.status(200).send({ data: saleInvoices });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private getCreditNoteAppliedInvoices = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: creditNoteId } = req.params;
|
||||
|
||||
try {
|
||||
const appliedInvoices =
|
||||
await this.getCreditAssociatedAppliedInvoices.getCreditAssociatedAppliedInvoices(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
);
|
||||
return res.status(200).send({ data: appliedInvoices });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param next
|
||||
*/
|
||||
handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'ENTRIES_ITEMS_IDS_NOT_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ENTRIES_ITEMS_IDS_NOT_EXISTS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_NOTE_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CREDIT_NOTE_NOT_FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_NOTE_ALREADY_OPENED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CREDIT_NOTE_ALREADY_OPENED', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'INVOICES_IDS_NOT_FOUND' ||
|
||||
error.errorType === 'INVOICES_NOT_DELIVERED_YET'
|
||||
) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'APPLIED_INVOICES_IDS_NOT_FOUND', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND', code: 900 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVOICES_HAS_NO_REMAINING_AMOUNT') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVOICES_HAS_NO_REMAINING_AMOUNT', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS', code: 1100 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_NOTE_HAS_APPLIED_INVOICES') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CREDIT_NOTE_HAS_APPLIED_INVOICES', code: 1200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'REFUND_CREDIT_NOTE_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'REFUND_CREDIT_NOTE_NOT_FOUND', code: 1300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4900,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
616
packages/server/src/api/controllers/Sales/PaymentReceives.ts
Normal file
616
packages/server/src/api/controllers/Sales/PaymentReceives.ts
Normal file
@@ -0,0 +1,616 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
IPaymentReceiveDTO,
|
||||
PaymentReceiveAction,
|
||||
SaleInvoiceAction,
|
||||
} from '@/interfaces';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives';
|
||||
import PaymentReceivesPages from '@/services/Sales/PaymentReceives/PaymentReceivesPages';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import PaymentReceiveNotifyBySms from '@/services/Sales/PaymentReceives/PaymentReceiveSmsNotify';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import GetPaymentReceivePdf from '@/services/Sales/PaymentReceives/GetPaymentReeceivePdf';
|
||||
|
||||
/**
|
||||
* Payments receives controller.
|
||||
* @service
|
||||
*/
|
||||
@Service()
|
||||
export default class PaymentReceivesController extends BaseController {
|
||||
@Inject()
|
||||
paymentReceiveService: PaymentReceiveService;
|
||||
|
||||
@Inject()
|
||||
PaymentReceivesPages: PaymentReceivesPages;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
paymentReceiveSmsNotify: PaymentReceiveNotifyBySms;
|
||||
|
||||
@Inject()
|
||||
paymentReceivePdf: GetPaymentReceivePdf;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(PaymentReceiveAction.Edit, AbilitySubject.PaymentReceive),
|
||||
this.editPaymentReceiveValidation,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editPaymentReceive.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/notify-by-sms',
|
||||
CheckPolicies(
|
||||
PaymentReceiveAction.NotifyBySms,
|
||||
AbilitySubject.PaymentReceive
|
||||
),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.paymentReceiveNotifyBySms),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/sms-details',
|
||||
CheckPolicies(
|
||||
PaymentReceiveAction.NotifyBySms,
|
||||
AbilitySubject.PaymentReceive
|
||||
),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.paymentReceiveSmsDetails),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(PaymentReceiveAction.Create, AbilitySubject.PaymentReceive),
|
||||
[...this.newPaymentReceiveValidation],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newPaymentReceive.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/edit-page',
|
||||
CheckPolicies(PaymentReceiveAction.Edit, AbilitySubject.PaymentReceive),
|
||||
this.paymentReceiveValidation,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getPaymentReceiveEditPage.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/new-page/entries',
|
||||
CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive),
|
||||
[query('customer_id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getPaymentReceiveNewPageEntries.bind(this)),
|
||||
this.getPaymentReceiveNewPageEntries.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/:id/invoices',
|
||||
CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive),
|
||||
this.paymentReceiveValidation,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getPaymentReceiveInvoices.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive),
|
||||
this.paymentReceiveValidation,
|
||||
this.asyncMiddleware(this.getPaymentReceive.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive),
|
||||
this.validatePaymentReceiveList,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getPaymentReceiveList.bind(this)),
|
||||
this.handleServiceErrors,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(PaymentReceiveAction.Delete, AbilitySubject.PaymentReceive),
|
||||
this.paymentReceiveValidation,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deletePaymentReceive.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment receive schema.
|
||||
* @return {Array}
|
||||
*/
|
||||
get paymentReceiveSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('payment_date').exists(),
|
||||
check('reference_no').optional(),
|
||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||
check('payment_receive_no').optional({ nullable: true }).trim().escape(),
|
||||
check('statement').optional().trim().escape(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.index').optional().isNumeric().toInt(),
|
||||
check('entries.*.invoice_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.payment_amount').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment receive list validation schema.
|
||||
*/
|
||||
get validatePaymentReceiveList(): ValidationChain[] {
|
||||
return [
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate payment receive parameters.
|
||||
*/
|
||||
get paymentReceiveValidation() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* New payment receive validation schema.
|
||||
* @return {Array}
|
||||
*/
|
||||
get newPaymentReceiveValidation() {
|
||||
return [...this.paymentReceiveSchema];
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit payment receive validation.
|
||||
*/
|
||||
get editPaymentReceiveValidation() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
...this.paymentReceiveSchema,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Records payment receive to the given customer with associated invoices.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async newPaymentReceive(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const storedPaymentReceive =
|
||||
await this.paymentReceiveService.createPaymentReceive(
|
||||
tenantId,
|
||||
paymentReceive,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: storedPaymentReceive.id,
|
||||
message: 'The payment receive has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the given payment receive.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async editPaymentReceive(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.paymentReceiveService.editPaymentReceive(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
paymentReceive,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: paymentReceiveId,
|
||||
message: 'The payment receive has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delets the given payment receive id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deletePaymentReceive(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
try {
|
||||
await this.paymentReceiveService.deletePaymentReceive(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: paymentReceiveId,
|
||||
message: 'The payment receive has been deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sale invoices that associated with the given payment receive.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getPaymentReceiveInvoices(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
try {
|
||||
const saleInvoices =
|
||||
await this.paymentReceiveService.getPaymentReceiveInvoices(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
|
||||
return res.status(200).send(this.transfromToResponse({ saleInvoices }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payment receive list with pagination metadata.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getPaymentReceiveList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { paymentReceives, pagination, filterMeta } =
|
||||
await this.paymentReceiveService.listPaymentReceives(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
payment_receives: this.transfromToResponse(paymentReceives),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payment receive new page receivable entries.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
*/
|
||||
async getPaymentReceiveNewPageEntries(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { customerId } = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const entries = await this.PaymentReceivesPages.getNewPageEntries(
|
||||
tenantId,
|
||||
customerId
|
||||
);
|
||||
return res.status(200).send({
|
||||
entries: this.transfromToResponse(entries),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given payment receive details.
|
||||
* @asycn
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async getPaymentReceiveEditPage(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
try {
|
||||
const { paymentReceive, entries } =
|
||||
await this.PaymentReceivesPages.getPaymentReceiveEditPage(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
payment_receive: this.transfromToResponse({ ...paymentReceive }),
|
||||
entries: this.transfromToResponse([...entries]),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the payment receive details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getPaymentReceive(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
try {
|
||||
const paymentReceive = await this.paymentReceiveService.getPaymentReceive(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
|
||||
const ACCEPT_TYPE = {
|
||||
APPLICATION_PDF: 'application/pdf',
|
||||
APPLICATION_JSON: 'application/json',
|
||||
};
|
||||
res.format({
|
||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
||||
return res.status(200).send({
|
||||
payment_receive: this.transfromToResponse(paymentReceive),
|
||||
});
|
||||
},
|
||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
||||
const pdfContent = await this.paymentReceivePdf.getPaymentReceivePdf(
|
||||
tenantId,
|
||||
paymentReceive
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment receive notfiy customer by sms.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public paymentReceiveNotifyBySms = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
try {
|
||||
const paymentReceive = await this.paymentReceiveSmsNotify.notifyBySms(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: paymentReceive.id,
|
||||
message: 'The payment notification has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public paymentReceiveSmsDetails = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
try {
|
||||
const smsDetails = await this.paymentReceiveSmsNotify.smsDetails(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: smsDetails,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param error
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
*/
|
||||
handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_RECEIVE_NO_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PAYMENT_RECEIVE_NO_EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_RECEIVE_NOT_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PAYMENT_RECEIVE_NOT_EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'DEPOSIT_ACCOUNT_INVALID_TYPE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'DEPOSIT_ACCOUNT_INVALID_TYPE', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVALID_PAYMENT_AMOUNT_INVALID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_PAYMENT_AMOUNT', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVOICES_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVOICES_IDS_NOT_FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_IDS_NOT_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVALID_PAYMENT_AMOUNT') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_PAYMENT_AMOUNT', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVOICES_NOT_DELIVERED_YET') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'INVOICES_NOT_DELIVERED_YET',
|
||||
code: 200,
|
||||
data: {
|
||||
not_delivered_invoices_ids:
|
||||
error.payload.notDeliveredInvoices.map(
|
||||
(invoice) => invoice.id
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_RECEIVE_NO_IS_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PAYMENT_RECEIVE_NO_IS_REQUIRED', code: 1100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE', code: 1200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_RECEIVE_NO_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PAYMENT_RECEIVE_NO_REQUIRED', code: 1300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_HAS_NO_PHONE_NUMBER') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_HAS_NO_PHONE_NUMBER', code: 1800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_SMS_NOTIFY_PHONE_INVALID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_SMS_NOTIFY_PHONE_INVALID', code: 1900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PAYMENT_ACCOUNT_CURRENCY_INVALID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PAYMENT_ACCOUNT_CURRENCY_INVALID', code: 2000 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
589
packages/server/src/api/controllers/Sales/SalesEstimates.ts
Normal file
589
packages/server/src/api/controllers/Sales/SalesEstimates.ts
Normal file
@@ -0,0 +1,589 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, matchedData } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ISaleEstimateDTO,
|
||||
SaleEstimateAction,
|
||||
SaleInvoiceAction,
|
||||
} from '@/interfaces';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import SaleEstimateService from '@/services/Sales/SalesEstimate';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import SaleEstimatesPdfService from '@/services/Sales/Estimates/SaleEstimatesPdf';
|
||||
import SaleEstimateNotifyBySms from '@/services/Sales/Estimates/SaleEstimateSmsNotify';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
const ACCEPT_TYPE = {
|
||||
APPLICATION_PDF: 'application/pdf',
|
||||
APPLICATION_JSON: 'application/json',
|
||||
};
|
||||
@Service()
|
||||
export default class SalesEstimatesController extends BaseController {
|
||||
@Inject()
|
||||
saleEstimateService: SaleEstimateService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
saleEstimatesPdf: SaleEstimatesPdfService;
|
||||
|
||||
@Inject()
|
||||
saleEstimateNotifySms: SaleEstimateNotifyBySms;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(SaleEstimateAction.Create, AbilitySubject.SaleEstimate),
|
||||
[...this.estimateValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/deliver',
|
||||
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
|
||||
[...this.validateSpecificEstimateSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deliverSaleEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/approve',
|
||||
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
|
||||
[this.validateSpecificEstimateSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.approveSaleEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/reject',
|
||||
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
|
||||
[this.validateSpecificEstimateSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.rejectSaleEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/sms-details',
|
||||
CheckPolicies(
|
||||
SaleEstimateAction.NotifyBySms,
|
||||
AbilitySubject.SaleEstimate
|
||||
),
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.saleEstimateSmsDetails),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/notify-by-sms',
|
||||
CheckPolicies(
|
||||
SaleEstimateAction.NotifyBySms,
|
||||
AbilitySubject.SaleEstimate
|
||||
),
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.saleEstimateNotifyBySms),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
|
||||
[
|
||||
...this.validateSpecificEstimateSchema,
|
||||
...this.estimateValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(SaleEstimateAction.Delete, AbilitySubject.SaleEstimate),
|
||||
[this.validateSpecificEstimateSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(SaleEstimateAction.View, AbilitySubject.SaleEstimate),
|
||||
this.validateSpecificEstimateSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(SaleEstimateAction.View, AbilitySubject.SaleEstimate),
|
||||
this.validateEstimateListSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getEstimates.bind(this)),
|
||||
this.handleServiceErrors,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate validation schema.
|
||||
*/
|
||||
get estimateValidationSchema() {
|
||||
return [
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('estimate_date').exists().isISO8601().toDate(),
|
||||
check('expiration_date').exists().isISO8601().toDate(),
|
||||
check('reference').optional(),
|
||||
check('estimate_number').optional().trim().escape(),
|
||||
check('delivered').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.quantity').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
|
||||
check('note').optional().trim().escape(),
|
||||
check('terms_conditions').optional().trim().escape(),
|
||||
check('send_to_email').optional().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific sale estimate validation schema.
|
||||
*/
|
||||
get validateSpecificEstimateSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sales estimates list validation schema.
|
||||
*/
|
||||
get validateEstimateListSchema() {
|
||||
return [
|
||||
query('view_slug').optional().isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle create a new estimate with associated entries.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response} res -
|
||||
*/
|
||||
async newEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const storedEstimate = await this.saleEstimateService.createEstimate(
|
||||
tenantId,
|
||||
estimateDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: storedEstimate.id,
|
||||
message: 'The sale estimate has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle update estimate details with associated entries.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
// Update estimate with associated estimate entries.
|
||||
await this.saleEstimateService.editEstimate(
|
||||
tenantId,
|
||||
estimateId,
|
||||
estimateDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: estimateId,
|
||||
message: 'The sale estimate has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given estimate with associated entries.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deleteEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.saleEstimateService.deleteEstimate(tenantId, estimateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: estimateId,
|
||||
message: 'The sale estimate has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver the given sale estimate.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deliverSaleEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.saleEstimateService.deliverSaleEstimate(tenantId, estimateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: estimateId,
|
||||
message: 'The sale estimate has been delivered successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the sale estimate as approved.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async approveSaleEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.saleEstimateService.approveSaleEstimate(tenantId, estimateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: estimateId,
|
||||
message: 'The sale estimate has been approved successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the sale estimate as rejected.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async rejectSaleEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.saleEstimateService.rejectSaleEstimate(tenantId, estimateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: estimateId,
|
||||
message: 'The sale estimate has been rejected successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given estimate with associated entries.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const estimate = await this.saleEstimateService.getEstimate(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
// Response formatter.
|
||||
res.format({
|
||||
// JSON content type.
|
||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
||||
return res.status(200).send(this.transfromToResponse({ estimate }));
|
||||
},
|
||||
// PDF content type.
|
||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
||||
const pdfContent = await this.saleEstimatesPdf.saleEstimatePdf(
|
||||
tenantId,
|
||||
estimate
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve estimates with pagination metadata.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getEstimates(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { salesEstimates, pagination, filterMeta } =
|
||||
await this.saleEstimateService.estimatesList(tenantId, filter);
|
||||
|
||||
res.format({
|
||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
||||
return res.status(200).send(
|
||||
this.transfromToResponse({
|
||||
salesEstimates,
|
||||
pagination,
|
||||
filterMeta,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
public saleEstimateNotifyBySms = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: estimateId } = req.params;
|
||||
|
||||
try {
|
||||
const saleEstimate = await this.saleEstimateNotifySms.notifyBySms(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: saleEstimate.id,
|
||||
message:
|
||||
'The sale estimate sms notification has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the sale estimate sms notification message details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public saleEstimateSmsDetails = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: estimateId } = req.params;
|
||||
|
||||
try {
|
||||
const estimateSmsDetails = await this.saleEstimateNotifySms.smsDetails(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: estimateSmsDetails,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'ITEMS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_IDS_NOT_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_PURCHASE_ABLE_ITEMS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_PURCHASABLE_ITEMS', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_ESTIMATE_NOT_FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_NUMBER_EXISTANCE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_SELL_ABLE_ITEMS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_SELL_ABLE_ITEMS', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_ALREADY_APPROVED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_NOT_DELIVERED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_ESTIMATE_NOT_DELIVERED', code: 1100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_ALREADY_REJECTED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_ESTIMATE_ALREADY_REJECTED', code: 1200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 1300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_NO_IS_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_ESTIMATE_NO_IS_REQUIRED', code: 1400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_CONVERTED_TO_INVOICE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE', code: 1500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_ALREADY_DELIVERED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_ESTIMATE_ALREADY_DELIVERED', code: 1600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_HAS_NO_PHONE_NUMBER') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_HAS_NO_PHONE_NUMBER', code: 1800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_SMS_NOTIFY_PHONE_INVALID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_SMS_NOTIFY_PHONE_INVALID', code: 1900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'WAREHOUSE_ID_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'WAREHOUSE_ID_NOT_FOUND', code: 5000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BRANCH_ID_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'BRANCH_ID_REQUIRED', code: 5100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BRANCH_ID_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'BRANCH_ID_NOT_FOUND', code: 5300 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
754
packages/server/src/api/controllers/Sales/SalesInvoices.ts
Normal file
754
packages/server/src/api/controllers/Sales/SalesInvoices.ts
Normal file
@@ -0,0 +1,754 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import BaseController from '../BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import SaleInvoiceService from '@/services/Sales/SalesInvoices';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
ISaleInvoiceDTO,
|
||||
ISaleInvoiceCreateDTO,
|
||||
SaleInvoiceAction,
|
||||
AbilitySubject,
|
||||
} 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 InvoicePaymentsService from '@/services/Sales/Invoices/InvoicePaymentsService';
|
||||
|
||||
const ACCEPT_TYPE = {
|
||||
APPLICATION_PDF: 'application/pdf',
|
||||
APPLICATION_JSON: 'application/json',
|
||||
};
|
||||
@Service()
|
||||
export default class SaleInvoicesController extends BaseController {
|
||||
@Inject()
|
||||
saleInvoiceService: SaleInvoiceService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
saleInvoicePdf: SaleInvoicePdf;
|
||||
|
||||
@Inject()
|
||||
saleInvoiceWriteoff: SaleInvoiceWriteoff;
|
||||
|
||||
@Inject()
|
||||
saleInvoiceSmsNotify: SaleInvoiceNotifyBySms;
|
||||
|
||||
@Inject()
|
||||
invoicePaymentsSerivce: InvoicePaymentsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(SaleInvoiceAction.Create, AbilitySubject.SaleInvoice),
|
||||
[
|
||||
...this.saleInvoiceValidationSchema,
|
||||
check('from_estimate_id').optional().isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newSaleInvoice.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/deliver',
|
||||
CheckPolicies(SaleInvoiceAction.Edit, AbilitySubject.SaleInvoice),
|
||||
[...this.specificSaleInvoiceValidation],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deliverSaleInvoice.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/writeoff',
|
||||
CheckPolicies(SaleInvoiceAction.Writeoff, AbilitySubject.SaleInvoice),
|
||||
[
|
||||
param('id').exists().isInt().toInt(),
|
||||
|
||||
check('expense_account_id').exists().isInt().toInt(),
|
||||
check('reason').exists().trim(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.writeoffSaleInvoice),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/writeoff/cancel',
|
||||
CheckPolicies(SaleInvoiceAction.Writeoff, AbilitySubject.SaleInvoice),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.cancelWrittenoffSaleInvoice),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/notify-by-sms',
|
||||
CheckPolicies(SaleInvoiceAction.NotifyBySms, AbilitySubject.SaleInvoice),
|
||||
[
|
||||
param('id').exists().isInt().toInt(),
|
||||
check('notification_key').exists().isIn(['details', 'reminder']),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.saleInvoiceNotifyBySms),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/sms-details',
|
||||
CheckPolicies(SaleInvoiceAction.NotifyBySms, AbilitySubject.SaleInvoice),
|
||||
[
|
||||
param('id').exists().isInt().toInt(),
|
||||
query('notification_key').exists().isIn(['details', 'reminder']),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.saleInvoiceSmsDetails),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(SaleInvoiceAction.Edit, AbilitySubject.SaleInvoice),
|
||||
[
|
||||
...this.saleInvoiceValidationSchema,
|
||||
...this.specificSaleInvoiceValidation,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editSaleInvoice.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(SaleInvoiceAction.Delete, AbilitySubject.SaleInvoice),
|
||||
this.specificSaleInvoiceValidation,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteSaleInvoice.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/payable',
|
||||
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
|
||||
[...this.dueSalesInvoicesListValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getPayableInvoices.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/payment-transactions',
|
||||
[param('id').exists().isString()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getInvoicePaymentTransactions),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
|
||||
this.specificSaleInvoiceValidation,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSaleInvoice.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
|
||||
this.saleInvoiceListValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSalesInvoices.bind(this)),
|
||||
this.handleServiceErrors,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sale invoice validation schema.
|
||||
*/
|
||||
get saleInvoiceValidationSchema() {
|
||||
return [
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('invoice_date').exists().isISO8601().toDate(),
|
||||
check('due_date').exists().isISO8601().toDate(),
|
||||
check('invoice_no').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('delivered').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('invoice_message').optional().trim().escape(),
|
||||
check('terms_conditions').optional().trim().escape(),
|
||||
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.project_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
|
||||
check('entries.*.project_ref_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.project_ref_type')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.toUpperCase()
|
||||
.isIn(['TASK', 'BILL', 'EXPENSE']),
|
||||
check('entries.*.project_ref_invoiced_amount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific sale invoice validation schema.
|
||||
*/
|
||||
get specificSaleInvoiceValidation() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sales invoices list validation schema.
|
||||
*/
|
||||
get saleInvoiceListValidationSchema() {
|
||||
return [
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Due sale invoice list validation schema.
|
||||
*/
|
||||
get dueSalesInvoicesListValidationSchema() {
|
||||
return [query('customer_id').optional().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sale invoice.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async newSaleInvoice(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const saleInvoiceDTO: ISaleInvoiceCreateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
// Creates a new sale invoice with associated entries.
|
||||
const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: storedSaleInvoice.id,
|
||||
message: 'The sale invoice has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit sale invoice details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async editSaleInvoice(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const saleInvoiceOTD: ISaleInvoiceDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
// Update the given sale invoice details.
|
||||
await this.saleInvoiceService.editSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
saleInvoiceOTD,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: saleInvoiceId,
|
||||
message: 'The sale invoice has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver the given sale invoice.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async deliverSaleInvoice(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
|
||||
try {
|
||||
await this.saleInvoiceService.deliverSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: saleInvoiceId,
|
||||
message: 'The given sale invoice has been delivered successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the sale invoice with associated entries and journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async deleteSaleInvoice(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
// Deletes the sale invoice with associated entries and journal transaction.
|
||||
await this.saleInvoiceService.deleteSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: saleInvoiceId,
|
||||
message: 'The sale invoice has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sale invoice with associated entries.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
*/
|
||||
async getSaleInvoice(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const saleInvoice = await this.saleInvoiceService.getSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
user
|
||||
);
|
||||
// Response formatter.
|
||||
res.format({
|
||||
// JSON content type.
|
||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transfromToResponse({ saleInvoice }));
|
||||
},
|
||||
// PDF content type.
|
||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
||||
const pdfContent = await this.saleInvoicePdf.saleInvoicePdf(
|
||||
tenantId,
|
||||
saleInvoice
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Retrieve paginated sales invoices with custom view metadata.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
public async getSalesInvoices(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
try {
|
||||
const { salesInvoices, filterMeta, pagination } =
|
||||
await this.saleInvoiceService.salesInvoicesList(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
sales_invoices: this.transfromToResponse(salesInvoices),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve due sales invoices.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
* @return {Response|void}
|
||||
*/
|
||||
public async getPayableInvoices(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { customerId } = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const salesInvoices = await this.saleInvoiceService.getPayableInvoices(
|
||||
tenantId,
|
||||
customerId
|
||||
);
|
||||
return res.status(200).send({
|
||||
sales_invoices: this.transfromToResponse(salesInvoices),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Written-off sale invoice.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param next
|
||||
*/
|
||||
public writeoffSaleInvoice = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
|
||||
const writeoffDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const saleInvoice = await this.saleInvoiceWriteoff.writeOff(
|
||||
tenantId,
|
||||
invoiceId,
|
||||
writeoffDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: saleInvoice.id,
|
||||
message: 'The given sale invoice has been writte-off successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel the written-off sale invoice.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param next
|
||||
*/
|
||||
public cancelWrittenoffSaleInvoice = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
|
||||
try {
|
||||
const saleInvoice = await this.saleInvoiceWriteoff.cancelWrittenoff(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: saleInvoice.id,
|
||||
message:
|
||||
'The given sale invoice has been canceled write-off successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sale invoice notfiy customer by sms.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public saleInvoiceNotifyBySms = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
|
||||
const invoiceNotifySmsDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const saleInvoice = await this.saleInvoiceSmsNotify.notifyBySms(
|
||||
tenantId,
|
||||
invoiceId,
|
||||
invoiceNotifySmsDTO.notificationKey
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: saleInvoice.id,
|
||||
message:
|
||||
'The sale invoice sms notification has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sale invoice SMS details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public saleInvoiceSmsDetails = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
const smsDetailsDTO = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const invoiceSmsDetails = await this.saleInvoiceSmsNotify.smsDetails(
|
||||
tenantId,
|
||||
invoiceId,
|
||||
smsDetailsDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: invoiceSmsDetails,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the invoice payment transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
public getInvoicePaymentTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
|
||||
try {
|
||||
const invoicePayments =
|
||||
await this.invoicePaymentsSerivce.getInvoicePayments(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
data: invoicePayments,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'INVOICE_NUMBER_NOT_UNIQUE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_INVOICE_NOT_FOUND') {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.INVOICE.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_ITEMS_IDS_NOT_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ENTRIES_ITEMS_IDS_NOT_EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_SELLABLE_ITEMS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_SELLABLE_ITEMS', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_INVOICE_NO_NOT_UNIQUE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_INVOICE_NO_NOT_UNIQUE', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS_NOT_FOUND', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_SELL_ABLE_ITEMS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_SELL_ABLE_ITEMS', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_INVOICE_ALREADY_DELIVERED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_INVOICE_ALREADY_DELIVERED', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES', code: 1100 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'FROM_SALE_ESTIMATE_NOT_FOUND', code: 1200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_CONVERTED_TO_INVOICE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE',
|
||||
code: 1300,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT', code: 1400 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_INVOICE_NO_IS_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_INVOICE_NO_IS_REQUIRED', code: 1500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_INVOICE_NOT_WRITTEN_OFF') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_INVOICE_NOT_WRITTEN_OFF', code: 1600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_INVOICE_ALREADY_WRITTEN_OFF') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_INVOICE_ALREADY_WRITTEN_OFF', code: 1700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_HAS_NO_PHONE_NUMBER') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_HAS_NO_PHONE_NUMBER', code: 1800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_SMS_NOTIFY_PHONE_INVALID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_SMS_NOTIFY_PHONE_INVALID', code: 1800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'SALE_INVOICE_HAS_APPLIED_TO_CREDIT_NOTES', code: 1900 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4900,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
502
packages/server/src/api/controllers/Sales/SalesReceipts.ts
Normal file
502
packages/server/src/api/controllers/Sales/SalesReceipts.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import SaleReceiptService from '@/services/Sales/SalesReceipts';
|
||||
import SaleReceiptsPdfService from '@/services/Sales/Receipts/SaleReceiptsPdfService';
|
||||
import BaseController from '../BaseController';
|
||||
import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import SaleReceiptNotifyBySms from '@/services/Sales/SaleReceiptNotifyBySms';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class SalesReceiptsController extends BaseController {
|
||||
@Inject()
|
||||
saleReceiptService: SaleReceiptService;
|
||||
|
||||
@Inject()
|
||||
saleReceiptsPdf: SaleReceiptsPdfService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
saleReceiptSmsNotify: SaleReceiptNotifyBySms;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:id/close',
|
||||
CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt),
|
||||
[...this.specificReceiptValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.closeSaleReceipt.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/notify-by-sms',
|
||||
CheckPolicies(SaleReceiptAction.NotifyBySms, AbilitySubject.SaleReceipt),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.asyncMiddleware(this.saleReceiptNotifyBySms),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/sms-details',
|
||||
CheckPolicies(SaleReceiptAction.NotifyBySms, AbilitySubject.SaleReceipt),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.saleReceiptSmsDetails,
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt),
|
||||
[
|
||||
...this.specificReceiptValidationSchema,
|
||||
...this.salesReceiptsValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editSaleReceipt.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(SaleReceiptAction.Create, AbilitySubject.SaleReceipt),
|
||||
this.salesReceiptsValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newSaleReceipt.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(SaleReceiptAction.Delete, AbilitySubject.SaleReceipt),
|
||||
this.specificReceiptValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteSaleReceipt.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
|
||||
this.listSalesReceiptsValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSalesReceipts.bind(this)),
|
||||
this.handleServiceErrors,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
|
||||
[...this.specificReceiptValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSaleReceipt.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sales receipt validation schema.
|
||||
* @return {Array}
|
||||
*/
|
||||
get salesReceiptsValidationSchema() {
|
||||
return [
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||
check('receipt_date').exists().isISO8601(),
|
||||
check('receipt_number').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('closed').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.quantity').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toInt(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('receipt_message').optional().trim().escape(),
|
||||
check('statement').optional().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific sale receipt validation schema.
|
||||
*/
|
||||
get specificReceiptValidationSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* List sales receipts validation schema.
|
||||
*/
|
||||
get listSalesReceiptsValidationSchema() {
|
||||
return [
|
||||
query('view_slug').optional().isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new receipt.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async newSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const saleReceiptDTO: ISaleReceiptDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
// Store the given sale receipt details with associated entries.
|
||||
const storedSaleReceipt = await this.saleReceiptService.createSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: storedSaleReceipt.id,
|
||||
message: 'Sale receipt has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the sale receipt with associated entries and journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deleteSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
try {
|
||||
// Deletes the sale receipt.
|
||||
await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: saleReceiptId,
|
||||
message: 'Sale receipt has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the sale receipt details with associated entries and re-write
|
||||
* journal transaction on the same date.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async editSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: saleReceiptId } = req.params;
|
||||
const saleReceipt = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
// Update the given sale receipt details.
|
||||
await this.saleReceiptService.editSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
saleReceipt
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: saleReceiptId,
|
||||
message: 'Sale receipt has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given the sale receipt as closed.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async closeSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
try {
|
||||
// Update the given sale receipt details.
|
||||
await this.saleReceiptService.closeSaleReceipt(tenantId, saleReceiptId);
|
||||
return res.status(200).send({
|
||||
id: saleReceiptId,
|
||||
message: 'Sale receipt has been closed successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing sales receipts.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getSalesReceipts(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, pagination, filterMeta } =
|
||||
await this.saleReceiptService.salesReceiptsList(tenantId, filter);
|
||||
|
||||
const response = this.transfromToResponse({
|
||||
data,
|
||||
pagination,
|
||||
filterMeta,
|
||||
});
|
||||
return res.status(200).send(response);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sale receipt with associated entries.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: saleReceiptId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const saleReceipt = await this.saleReceiptService.getSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
|
||||
res.format({
|
||||
'application/json': () => {
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transfromToResponse({ saleReceipt }));
|
||||
},
|
||||
'application/pdf': async () => {
|
||||
const pdfContent = await this.saleReceiptsPdf.saleReceiptPdf(
|
||||
tenantId,
|
||||
saleReceipt
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sale receipt notification via SMS.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public saleReceiptNotifyBySms = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: receiptId } = req.params;
|
||||
|
||||
try {
|
||||
const saleReceipt = await this.saleReceiptSmsNotify.notifyBySms(
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: saleReceipt.id,
|
||||
message:
|
||||
'The sale receipt notification via sms has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sale receipt sms details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public saleReceiptSmsDetails = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: receiptId } = req.params;
|
||||
|
||||
try {
|
||||
const smsDetails = await this.saleReceiptSmsNotify.smsDetails(
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: smsDetails,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'SALE_RECEIPT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_RECEIPT_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'DEPOSIT_ACCOUNT_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ENTRIES_IDS_NOT_FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_SELL_ABLE_ITEMS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_SELL_ABLE_ITEMS', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE.RECEIPT.NOT.FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'DEPOSIT.ACCOUNT.NOT.EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_RECEIPT_NUMBER_NOT_UNIQUE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_RECEIPT_IS_ALREADY_CLOSED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_RECEIPT_IS_ALREADY_CLOSED', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_RECEIPT_NO_IS_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'SALE_RECEIPT_NO_IS_REQUIRED',
|
||||
message: 'The sale receipt number is required.',
|
||||
code: 1100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_HAS_NO_PHONE_NUMBER') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_HAS_NO_PHONE_NUMBER', code: 1800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_SMS_NOTIFY_PHONE_INVALID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_SMS_NOTIFY_PHONE_INVALID', code: 1900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'WAREHOUSE_ID_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'WAREHOUSE_ID_NOT_FOUND', code: 5000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BRANCH_ID_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'BRANCH_ID_REQUIRED', code: 5100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BRANCH_ID_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'BRANCH_ID_NOT_FOUND', code: 5300 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
24
packages/server/src/api/controllers/Sales/index.ts
Normal file
24
packages/server/src/api/controllers/Sales/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Router } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
import SalesEstimates from './SalesEstimates';
|
||||
import SalesReceipts from './SalesReceipts';
|
||||
import SalesInvoices from './SalesInvoices'
|
||||
import PaymentReceives from './PaymentReceives';
|
||||
import CreditNotes from './CreditNotes';
|
||||
@Service()
|
||||
export default class SalesController {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/invoices', Container.get(SalesInvoices).router());
|
||||
router.use('/estimates', Container.get(SalesEstimates).router());
|
||||
router.use('/receipts', Container.get(SalesReceipts).router());
|
||||
router.use('/payment_receives', Container.get(PaymentReceives).router());
|
||||
router.use('/credit_notes', Container.get(CreditNotes).router())
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, NextFunction, Response } from 'express';
|
||||
import { check } from 'express-validator';
|
||||
import { Request } from 'express-validator/src/base';
|
||||
import EasySmsIntegration from '@/services/SmsIntegration/EasySmsIntegration';
|
||||
import BaseController from '../BaseController';
|
||||
|
||||
@Service()
|
||||
export default class EasySmsIntegrationController extends BaseController {
|
||||
@Inject()
|
||||
easySmsIntegrationService: EasySmsIntegration;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
*/
|
||||
public router = () => {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/easysms/integrate',
|
||||
[check('token').exists()],
|
||||
this.integrationEasySms
|
||||
);
|
||||
router.post(
|
||||
'/easysms/disconnect',
|
||||
this.disconnectEasysms
|
||||
)
|
||||
router.get('/easysms', this.getIntegrationMeta);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
/**
|
||||
* Easysms integration API.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
private integrationEasySms = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const easysmsIntegrateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.easySmsIntegrationService.integrate(
|
||||
tenantId,
|
||||
easysmsIntegrateDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
message:
|
||||
'The system has been integrated with Easysms sms gateway successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the Easysms integration meta.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private getIntegrationMeta = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const data = await this.easySmsIntegrationService.getIntegrationMeta(
|
||||
tenantId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private disconnectEasysms = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.easySmsIntegrationService.disconnect(
|
||||
tenantId,
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'The sms gateway integration has been disconnected successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
114
packages/server/src/api/controllers/Settings/Settings.ts
Normal file
114
packages/server/src/api/controllers/Settings/Settings.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { body, query } from 'express-validator';
|
||||
import { pick } from 'lodash';
|
||||
import { IOptionDTO, IOptionsDTO } from '@/interfaces';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { AbilitySubject, PreferencesAction } from '@/interfaces';
|
||||
import SettingsService from '@/services/Settings/SettingsService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class SettingsController extends BaseController {
|
||||
@Inject()
|
||||
settingsService: SettingsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(PreferencesAction.Mutate, AbilitySubject.Preferences),
|
||||
this.saveSettingsValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.saveSettings.bind(this))
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
this.getSettingsSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSettings.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings validation schema.
|
||||
*/
|
||||
private get saveSettingsValidationSchema() {
|
||||
return [
|
||||
body('options').isArray({ min: 1 }),
|
||||
body('options.*.key').exists().trim().isLength({ min: 1 }),
|
||||
body('options.*.value').exists().trim(),
|
||||
body('options.*.group').exists().trim().isLength({ min: 1 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the application options from the storage.
|
||||
*/
|
||||
private get getSettingsSchema() {
|
||||
return [
|
||||
query('key').optional().trim().escape(),
|
||||
query('group').optional().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given options to the storage.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async saveSettings(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const optionsDTO: IOptionsDTO = this.matchedBodyData(req);
|
||||
const { settings } = req;
|
||||
|
||||
const errorReasons: { type: string; code: number; keys: [] }[] = [];
|
||||
const notDefinedOptions = this.settingsService.validateNotDefinedSettings(
|
||||
tenantId,
|
||||
optionsDTO.options
|
||||
);
|
||||
|
||||
if (notDefinedOptions.length) {
|
||||
errorReasons.push({
|
||||
type: 'OPTIONS.KEY.NOT.DEFINED',
|
||||
code: 200,
|
||||
keys: notDefinedOptions.map((o) => ({ ...pick(o, ['key', 'group']) })),
|
||||
});
|
||||
}
|
||||
if (errorReasons.length) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
optionsDTO.options.forEach((option: IOptionDTO) => {
|
||||
settings.set({ ...option });
|
||||
});
|
||||
try {
|
||||
await settings.save();
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'OPTIONS.SAVED.SUCCESSFULLY',
|
||||
message: 'Options have been saved successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve settings.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
public getSettings(req: Request, res: Response) {
|
||||
const { settings } = req;
|
||||
const allSettings = settings.all();
|
||||
|
||||
return res.status(200).send({ settings: allSettings });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { check, oneOf, param } from 'express-validator';
|
||||
import { Router, Response, Request, NextFunction } from 'express';
|
||||
|
||||
import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
|
||||
import BaseController from '../BaseController';
|
||||
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
AbilitySubject,
|
||||
PreferencesAction,
|
||||
IEditSmsNotificationDTO,
|
||||
} from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class SettingsController extends BaseController {
|
||||
@Inject()
|
||||
smsNotificationsSettings: SmsNotificationsSettingsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/sms-notifications',
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.smsNotifications),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/sms-notification/:notification_key',
|
||||
[param('notification_key').exists().isString()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.smsNotification),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/sms-notification',
|
||||
CheckPolicies(PreferencesAction.Mutate, AbilitySubject.Preferences),
|
||||
[
|
||||
check('notification_key').exists(),
|
||||
oneOf([
|
||||
check('message_text').exists(),
|
||||
check('is_notification_enabled').exists().isBoolean().toBoolean(),
|
||||
]),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.updateSmsNotification),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sms notifications.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private smsNotifications = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const notifications =
|
||||
await this.smsNotificationsSettings.smsNotificationsList(tenantId);
|
||||
|
||||
return res.status(200).send({ notifications });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the sms notification details from the given notification key.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private smsNotification = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { notification_key: notificationKey } = req.params;
|
||||
|
||||
try {
|
||||
const notification =
|
||||
await this.smsNotificationsSettings.getSmsNotificationMeta(
|
||||
tenantId,
|
||||
notificationKey
|
||||
);
|
||||
|
||||
return res.status(200).send({ notification });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the given sms notification key.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private updateSmsNotification = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const editDTO: IEditSmsNotificationDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.smsNotificationsSettings.editSmsNotificationMessage(
|
||||
tenantId,
|
||||
editDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'Sms notification settings has been updated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceErrors = (
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'SMS_NOTIFICATION_KEY_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SMS_NOTIFICATION_KEY_NOT_FOUND', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'UNSUPPORTED_SMS_MESSAGE_VARIABLES') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'UNSUPPORTED_SMS_MESSAGE_VARIABLES',
|
||||
code: 1100,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
};
|
||||
}
|
||||
23
packages/server/src/api/controllers/Settings/index.ts
Normal file
23
packages/server/src/api/controllers/Settings/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
import SmsNotificationSettings from './SmsNotificationsSettings';
|
||||
import Settings from './Settings';
|
||||
import EasySmsIntegrationController from './EasySmsIntegration';
|
||||
import { AbilitySubject, PreferencesAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class SettingsController {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/', Container.get(EasySmsIntegrationController).router());
|
||||
router.use('/', Container.get(SmsNotificationSettings).router());
|
||||
router.use('/', Container.get(Settings).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
102
packages/server/src/api/controllers/Setup.ts
Normal file
102
packages/server/src/api/controllers/Setup.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, ValidationChain } from 'express-validator';
|
||||
import BaseController from './BaseController';
|
||||
import SetupService from '@/services/Setup/SetupService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IOrganizationSetupDTO } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
// Middlewares
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
|
||||
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
|
||||
|
||||
@Service()
|
||||
export default class SetupController extends BaseController {
|
||||
@Inject()
|
||||
setupService: SetupService;
|
||||
|
||||
router() {
|
||||
const router = Router('/setup');
|
||||
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
router.use(SubscriptionMiddleware('main'));
|
||||
router.use(EnsureTenantIsInitialized);
|
||||
router.use(SettingsMiddleware);
|
||||
router.post(
|
||||
'/organization',
|
||||
this.organizationSetupSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.organizationSetup.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization setup schema.
|
||||
*/
|
||||
private get organizationSetupSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('organization_name').exists().trim(),
|
||||
check('base_currency').exists(),
|
||||
check('time_zone').exists(),
|
||||
check('fiscal_year').exists(),
|
||||
check('industry').optional(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization setup.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
async organizationSetup(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const setupDTO: IOrganizationSetupDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.setupService.organizationSetup(tenantId, setupDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'The setup settings set successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'TENANT_IS_ALREADY_SETUPED') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT_IS_ALREADY_SETUPED', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BASE_CURRENCY_INVALID') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BASE_CURRENCY_INVALID', code: 110 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
250
packages/server/src/api/controllers/Subscription/Licenses.ts
Normal file
250
packages/server/src/api/controllers/Subscription/Licenses.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, oneOf, ValidationChain } from 'express-validator';
|
||||
import basicAuth from 'express-basic-auth';
|
||||
import config from '@/config';
|
||||
import { License } from '@/system/models';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import LicenseService from '@/services/Payment/License';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { ILicensesFilter, ISendLicenseDTO } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class LicensesController extends BaseController {
|
||||
@Inject()
|
||||
licenseService: LicenseService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(
|
||||
basicAuth({
|
||||
users: {
|
||||
[config.licensesAuth.user]: config.licensesAuth.password,
|
||||
},
|
||||
challenge: true,
|
||||
})
|
||||
);
|
||||
router.post(
|
||||
'/generate',
|
||||
this.generateLicenseSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.generateLicense.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
);
|
||||
router.post(
|
||||
'/disable/:licenseId',
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.disableLicense.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
);
|
||||
router.post(
|
||||
'/send',
|
||||
this.sendLicenseSchemaValidation,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendLicense.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
);
|
||||
router.delete(
|
||||
'/:licenseId',
|
||||
asyncMiddleware(this.deleteLicense.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
);
|
||||
router.get('/', asyncMiddleware(this.listLicenses.bind(this)));
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate license validation schema.
|
||||
*/
|
||||
get generateLicenseSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('loop').exists().isNumeric().toInt(),
|
||||
check('period').exists().isNumeric().toInt(),
|
||||
check('period_interval')
|
||||
.exists()
|
||||
.isIn(['month', 'months', 'year', 'years', 'day', 'days']),
|
||||
check('plan_slug').exists().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific license validation schema.
|
||||
*/
|
||||
get specificLicenseSchema(): ValidationChain[] {
|
||||
return [
|
||||
oneOf(
|
||||
[check('license_id').exists().isNumeric().toInt()],
|
||||
[check('license_code').exists().isNumeric().toInt()]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send license validation schema.
|
||||
*/
|
||||
get sendLicenseSchemaValidation(): ValidationChain[] {
|
||||
return [
|
||||
check('period').exists().isNumeric(),
|
||||
check('period_interval').exists().trim().escape(),
|
||||
check('plan_slug').exists().trim().escape(),
|
||||
oneOf([
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('email').exists().trim().escape(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate licenses codes with given period in bulk.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async generateLicense(req: Request, res: Response, next: Function) {
|
||||
const { loop = 10, period, periodInterval, planSlug } = this.matchedBodyData(
|
||||
req
|
||||
);
|
||||
|
||||
try {
|
||||
await this.licenseService.generateLicenses(
|
||||
loop,
|
||||
period,
|
||||
periodInterval,
|
||||
planSlug
|
||||
);
|
||||
return res.status(200).send({
|
||||
code: 100,
|
||||
type: 'LICENSEES.GENERATED.SUCCESSFULLY',
|
||||
message: 'The licenses have been generated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the given license on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async disableLicense(req: Request, res: Response, next: Function) {
|
||||
const { licenseId } = req.params;
|
||||
|
||||
try {
|
||||
await this.licenseService.disableLicense(licenseId);
|
||||
|
||||
return res.status(200).send({ license_id: licenseId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given license code on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteLicense(req: Request, res: Response, next: Function) {
|
||||
const { licenseId } = req.params;
|
||||
|
||||
try {
|
||||
await this.licenseService.deleteLicense(licenseId);
|
||||
|
||||
return res.status(200).send({ license_id: licenseId });
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send license code in the given period to the customer via email or phone number
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async sendLicense(req: Request, res: Response, next: Function) {
|
||||
const sendLicenseDTO: ISendLicenseDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.licenseService.sendLicenseToCustomer(sendLicenseDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 100,
|
||||
code: 'LICENSE.CODE.SENT',
|
||||
message: 'The license has been sent to the given customer.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing licenses.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async listLicenses(req: Request, res: Response) {
|
||||
const filter: ILicensesFilter = {
|
||||
disabled: false,
|
||||
used: false,
|
||||
sent: false,
|
||||
active: false,
|
||||
...req.query,
|
||||
};
|
||||
const licenses = await License.query().onBuild((builder) => {
|
||||
builder.modify('filter', filter);
|
||||
builder.orderBy('createdAt', 'ASC');
|
||||
});
|
||||
return res.status(200).send({ licenses });
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches all service errors.
|
||||
*/
|
||||
catchServiceErrors(error, req: Request, res: Response, next: NextFunction) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'PLAN_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{
|
||||
type: 'PLAN.NOT.FOUND',
|
||||
code: 100,
|
||||
message: 'The given plan not found.',
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LICENSE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{
|
||||
type: 'LICENSE_NOT_FOUND',
|
||||
code: 200,
|
||||
message: 'The given license id not found.'
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LICENSE_ALREADY_DISABLED') {
|
||||
return res.status(400).send({
|
||||
errors: [{
|
||||
type: 'LICENSE.ALREADY.DISABLED',
|
||||
code: 200,
|
||||
message: 'License is already disabled.'
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NO_AVALIABLE_LICENSE_CODE') {
|
||||
return res.status(400).send({
|
||||
status: 110,
|
||||
message: 'There is no licenses availiable right now with the given period and plan.',
|
||||
code: 'NO.AVALIABLE.LICENSE.CODE',
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { Request, Response } from 'express';
|
||||
import { Plan } from '@/system/models';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
||||
|
||||
export default class PaymentMethodController extends BaseController {
|
||||
@Inject()
|
||||
subscriptionService: SubscriptionService;
|
||||
|
||||
/**
|
||||
* Validate the given plan slug exists on the storage.
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*
|
||||
* @return {Response|void}
|
||||
*/
|
||||
async validatePlanSlugExistance(req: Request, res: Response, next: Function) {
|
||||
const { planSlug } = this.matchedBodyData(req);
|
||||
const foundPlan = await Plan.query().where('slug', planSlug).first();
|
||||
|
||||
if (!foundPlan) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PLAN.SLUG.NOT.EXISTS', code: 110 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NextFunction, Router, Request, Response } from 'express';
|
||||
import { check } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import PaymentMethodController from '@/api/controllers/Subscription/PaymentMethod';
|
||||
import {
|
||||
NotAllowedChangeSubscriptionPlan,
|
||||
NoPaymentModelWithPricedPlan,
|
||||
PaymentAmountInvalidWithPlan,
|
||||
PaymentInputInvalid,
|
||||
VoucherCodeRequired,
|
||||
} from '@/exceptions';
|
||||
import { ILicensePaymentModel } from '@/interfaces';
|
||||
import instance from 'tsyringe/dist/typings/dependency-container';
|
||||
|
||||
@Service()
|
||||
export default class PaymentViaLicenseController extends PaymentMethodController {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/payment',
|
||||
this.paymentViaLicenseSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
|
||||
asyncMiddleware(this.paymentViaLicense.bind(this)),
|
||||
this.handleErrors,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment via license validation schema.
|
||||
*/
|
||||
get paymentViaLicenseSchema() {
|
||||
return [
|
||||
check('plan_slug').exists().trim().escape(),
|
||||
check('license_code').exists().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the subscription payment via license code.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async paymentViaLicense(req: Request, res: Response, next: Function) {
|
||||
const { planSlug, licenseCode } = this.matchedBodyData(req);
|
||||
const { tenant } = req;
|
||||
|
||||
try {
|
||||
const licenseModel: ILicensePaymentModel = { licenseCode };
|
||||
|
||||
await this.subscriptionService.subscriptionViaLicense(
|
||||
tenant.id,
|
||||
planSlug,
|
||||
licenseModel
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'PAYMENT.SUCCESSFULLY.MADE',
|
||||
message: 'Payment via license has been made successfully.',
|
||||
});
|
||||
} catch (exception) {
|
||||
next(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleErrors(
|
||||
exception: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const errorReasons = [];
|
||||
|
||||
if (exception instanceof VoucherCodeRequired) {
|
||||
errorReasons.push({
|
||||
type: 'VOUCHER_CODE_REQUIRED',
|
||||
code: 100,
|
||||
});
|
||||
}
|
||||
if (exception instanceof NoPaymentModelWithPricedPlan) {
|
||||
errorReasons.push({
|
||||
type: 'NO_PAYMENT_WITH_PRICED_PLAN',
|
||||
code: 140,
|
||||
});
|
||||
}
|
||||
if (exception instanceof NotAllowedChangeSubscriptionPlan) {
|
||||
errorReasons.push({
|
||||
type: 'NOT.ALLOWED.RENEW.SUBSCRIPTION.WHILE.ACTIVE',
|
||||
code: 120,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
if (exception instanceof PaymentInputInvalid) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'LICENSE.CODE.IS.INVALID', code: 120 }],
|
||||
});
|
||||
}
|
||||
if (exception instanceof PaymentAmountInvalidWithPlan) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'LICENSE.NOT.FOR.GIVEN.PLAN' }],
|
||||
});
|
||||
}
|
||||
next(exception);
|
||||
}
|
||||
}
|
||||
49
packages/server/src/api/controllers/Subscription/index.ts
Normal file
49
packages/server/src/api/controllers/Subscription/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import PaymentViaLicenseController from '@/api/controllers/Subscription/PaymentViaLicense';
|
||||
import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionController {
|
||||
@Inject()
|
||||
subscriptionService: SubscriptionService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.use('/license', Container.get(PaymentViaLicenseController).router());
|
||||
router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all subscriptions of the authenticated user's tenant.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getSubscriptions(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const subscriptions = await this.subscriptionService.getSubscriptions(
|
||||
tenantId
|
||||
);
|
||||
return res.status(200).send({ subscriptions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
284
packages/server/src/api/controllers/TransactionsLocking/index.ts
Normal file
284
packages/server/src/api/controllers/TransactionsLocking/index.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { check, param } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import TransactionsLockingService from '@/services/TransactionsLocking/CommandTransactionsLockingService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, AccountAction } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import QueryTransactionsLocking from '@/services/TransactionsLocking/QueryTransactionsLocking';
|
||||
|
||||
@Service()
|
||||
export default class TransactionsLockingController extends BaseController {
|
||||
@Inject()
|
||||
private transactionsLockingService: TransactionsLockingService;
|
||||
|
||||
@Inject()
|
||||
private queryTransactionsLocking: QueryTransactionsLocking;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.put(
|
||||
'/lock',
|
||||
CheckPolicies(AccountAction.TransactionsLocking, AbilitySubject.Account),
|
||||
[
|
||||
check('module')
|
||||
.exists()
|
||||
.isIn(['all', 'sales', 'purchases', 'financial']),
|
||||
check('lock_to_date').exists().isISO8601().toDate(),
|
||||
check('reason').exists().trim(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.commandTransactionsLocking),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.put(
|
||||
'/cancel-lock',
|
||||
CheckPolicies(AccountAction.TransactionsLocking, AbilitySubject.Account),
|
||||
[check('module').exists(), check('reason').exists().trim()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.cancelTransactionsLocking),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.put(
|
||||
'/unlock-partial',
|
||||
CheckPolicies(AccountAction.TransactionsLocking, AbilitySubject.Account),
|
||||
[
|
||||
check('module').exists(),
|
||||
check('unlock_from_date').exists().isISO8601().toDate(),
|
||||
check('unlock_to_date').exists().isISO8601().toDate(),
|
||||
check('reason').exists().trim(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.unlockTransactionsLockingBetweenPeriod),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.put(
|
||||
'/cancel-unlock-partial',
|
||||
CheckPolicies(AccountAction.TransactionsLocking, AbilitySubject.Account),
|
||||
[
|
||||
check('module').exists(),
|
||||
check('reason').optional({ nullable: true }).trim(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.cancelPartialUnlocking),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getTransactionLockingMetaList),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:module',
|
||||
[param('module').exists()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getTransactionLockingMeta),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts types list.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @return {Response}
|
||||
*/
|
||||
private commandTransactionsLocking = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { module, ...allTransactionsDTO } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const transactionMeta =
|
||||
await this.transactionsLockingService.commandTransactionsLocking(
|
||||
tenantId,
|
||||
module,
|
||||
allTransactionsDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'All transactions locking has been submit successfully.',
|
||||
data: transactionMeta,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unlock transactions locking between the given periods.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private unlockTransactionsLockingBetweenPeriod = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { module, ...unlockDTO } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const transactionMeta =
|
||||
await this.transactionsLockingService.unlockTransactionsLockingPartially(
|
||||
tenantId,
|
||||
module,
|
||||
unlockDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
message:
|
||||
'Transactions locking haas been unlocked partially successfully.',
|
||||
data: transactionMeta,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel full transactions locking of the given module.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private cancelTransactionsLocking = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { module, ...cancelLockingDTO } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.transactionsLockingService.cancelTransactionLocking(
|
||||
tenantId,
|
||||
module,
|
||||
cancelLockingDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'Transactions locking has been canceled successfully.',
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel transaction partial unlocking.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private cancelPartialUnlocking = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { module } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const transactionMeta =
|
||||
await this.transactionsLockingService.cancelPartialTransactionsUnlock(
|
||||
tenantId,
|
||||
module
|
||||
);
|
||||
return res.status(200).send({
|
||||
message:
|
||||
'Partial transaction unlocking has been canceled successfully.',
|
||||
data: transactionMeta,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
private getTransactionLockingMeta = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { module } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.queryTransactionsLocking.getTransactionsLockingModuleMeta(
|
||||
tenantId,
|
||||
module
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve transactions locking meta list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getTransactionLockingMetaList = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { module } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.queryTransactionsLocking.getTransactionsLockingList(
|
||||
tenantId
|
||||
);
|
||||
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the service errors.
|
||||
* @param {Error} error -
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private handleServiceErrors = (
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'TRANSACTION_LOCKING_ALL') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'TRANSACTION_LOCKING_ALL', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_LOCKING_MODULE_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'TRANSACTIONS_LOCKING_MODULE_NOT_FOUND', code: 100 },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { chain, mapKeys } from 'lodash';
|
||||
|
||||
export const getTransactionsLockingSettingsSchema = (modules: string[]) => {
|
||||
const moduleSchema = {
|
||||
active: { type: 'boolean' },
|
||||
lock_to_date: { type: 'date' },
|
||||
unlock_from_date: { type: 'date' },
|
||||
unlock_to_date: { type: 'date' },
|
||||
lock_reason: { type: 'string' },
|
||||
unlock_reason: { type: 'string' },
|
||||
};
|
||||
return chain(modules)
|
||||
.map((module: string) => {
|
||||
return mapKeys(moduleSchema, (value, key: string) => `${module}.${key}`);
|
||||
})
|
||||
.flattenDeep()
|
||||
.reduce((result, value) => {
|
||||
return {
|
||||
...result,
|
||||
...value,
|
||||
};
|
||||
}, {})
|
||||
.value();
|
||||
};
|
||||
289
packages/server/src/api/controllers/Users.ts
Normal file
289
packages/server/src/api/controllers/Users.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { check, query, param } from 'express-validator';
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import UsersService from '@/services/Users/UsersService';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import { ServiceError, ServiceErrors } from '@/exceptions';
|
||||
import { IEditUserDTO, ISystemUserDTO } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class UsersController extends BaseController {
|
||||
@Inject()
|
||||
usersService: UsersService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.put(
|
||||
'/:id/inactivate',
|
||||
[...this.specificUserSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.inactivateUser.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.put(
|
||||
'/:id/activate',
|
||||
[...this.specificUserSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.activateUser.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
|
||||
check('first_name').exists(),
|
||||
check('last_name').exists(),
|
||||
check('email').exists().isEmail(),
|
||||
check('phone_number').optional().isMobilePhone(),
|
||||
check('role_id').exists().isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editUser.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
this.listUsersSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.listUsers.bind(this))
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[...this.specificUserSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getUser.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[...this.specificUserSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteUser.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* User DTO Schema.
|
||||
*/
|
||||
get userDTOSchema() {
|
||||
return [];
|
||||
}
|
||||
|
||||
get specificUserSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
get listUsersSchema() {
|
||||
return [
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit details of the given user.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response|void}
|
||||
*/
|
||||
async editUser(req: Request, res: Response, next: NextFunction) {
|
||||
const editUserDTO: IEditUserDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user: authorizedUser } = req;
|
||||
const { id: userId } = req.params;
|
||||
|
||||
try {
|
||||
await this.usersService.editUser(
|
||||
tenantId,
|
||||
userId,
|
||||
editUserDTO,
|
||||
authorizedUser
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: userId,
|
||||
message: 'The user has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft deleting the given user.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response|void}
|
||||
*/
|
||||
async deleteUser(req: Request, res: Response, next: Function) {
|
||||
const { id } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.usersService.deleteUser(tenantId, id);
|
||||
|
||||
return res.status(200).send({
|
||||
id,
|
||||
message: 'The user has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user details of the given user id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response|void}
|
||||
*/
|
||||
async getUser(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: userId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const user = await this.usersService.getUser(tenantId, userId);
|
||||
return res.status(200).send({ user });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of users.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response|void}
|
||||
*/
|
||||
async listUsers(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
try {
|
||||
const users = await this.usersService.getList(tenantId);
|
||||
|
||||
return res.status(200).send({ users });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the given user.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async activateUser(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: userId } = req.params;
|
||||
|
||||
try {
|
||||
await this.usersService.activateUser(tenantId, userId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: userId,
|
||||
message: 'The user has been activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivate the given user.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response|void}
|
||||
*/
|
||||
async inactivateUser(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: userId } = req.params;
|
||||
|
||||
try {
|
||||
await this.usersService.inactivateUser(tenantId, userId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: userId,
|
||||
message: 'The user has been inactivated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches all users service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
catchServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'USER_NOT_FOUND') {
|
||||
return res.boom.badRequest('User not found.', {
|
||||
errors: [{ type: 'USER.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_ALREADY_ACTIVE') {
|
||||
return res.boom.badRequest('User is already active.', {
|
||||
errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_ALREADY_INACTIVE') {
|
||||
return res.boom.badRequest('User is already inactive.', {
|
||||
errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_SAME_THE_AUTHORIZED_USER') {
|
||||
return res.boom.badRequest(
|
||||
'You could not activate/inactivate the same authorized user.',
|
||||
{
|
||||
errors: [
|
||||
{ type: 'CANNOT.TOGGLE.ACTIVATE.AUTHORIZED.USER', code: 300 },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'CANNOT_DELETE_LAST_USER') {
|
||||
return res.boom.badRequest(
|
||||
'Cannot delete last user in the organization.',
|
||||
{ errors: [{ type: 'CANNOT_DELETE_LAST_USER', code: 400 }] }
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'EMAIL_ALREADY_EXISTS') {
|
||||
return res.boom.badRequest('Exmail is already exists.', {
|
||||
errors: [{ type: 'EMAIL_ALREADY_EXISTS', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PHONE_NUMBER_ALREADY_EXIST') {
|
||||
return res.boom.badRequest('Phone number is already exists.', {
|
||||
errors: [{ type: 'PHONE_NUMBER_ALREADY_EXIST', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CANNOT_AUTHORIZED_USER_MUTATE_ROLE') {
|
||||
return res.boom.badRequest('Cannout mutate authorized user role.', {
|
||||
errors: [{ type: 'CANNOT_AUTHORIZED_USER_MUTATE_ROLE', code: 700 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
123
packages/server/src/api/controllers/Views.ts
Normal file
123
packages/server/src/api/controllers/Views.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, NextFunction, Response } from 'express';
|
||||
import { check, param } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import ViewsService from '@/services/Views/ViewsService';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { IViewDTO, IViewEditDTO } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export default class ViewsController extends BaseController {
|
||||
@Inject()
|
||||
viewsService: ViewsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/resource/:resource_model',
|
||||
[...this.viewsListSchemaValidation],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.listResourceViews.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom views list validation schema.
|
||||
*/
|
||||
get viewsListSchemaValidation() {
|
||||
return [param('resource_model').exists().trim().escape()];
|
||||
}
|
||||
|
||||
/**
|
||||
* List all views that associated with the given resource.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
async listResourceViews(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { resource_model: resourceModel } = req.params;
|
||||
|
||||
try {
|
||||
const views = await this.viewsService.listResourceViews(
|
||||
tenantId,
|
||||
resourceModel
|
||||
);
|
||||
return res.status(200).send({
|
||||
views: this.transfromToResponse(views, ['name', 'columns.label'], req),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'VIEW_NAME_NOT_UNIQUE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VIEW_NAME_NOT_UNIQUE', code: 110 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'RESOURCE_MODEL_NOT_FOUND', code: 150 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVALID_LOGIC_EXPRESSION') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === '') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === '') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'COLUMNS_NOT_EXIST', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VIEW_NOT_FOUND') {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VIEW_PREDEFINED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'RESOURCE_FIELDS_KEYS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'RESOURCE_FIELDS_KEYS_NOT_FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'RESOURCE_COLUMNS_KEYS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND', code: 310 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { query, check, param } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { WarehouseTransferApplication } from '@/services/Warehouses/WarehousesTransfers/WarehouseTransferApplication';
|
||||
import {
|
||||
Features,
|
||||
ICreateWarehouseTransferDTO,
|
||||
IEditWarehouseTransferDTO,
|
||||
} from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { FeatureActivationGuard } from '@/api/middleware/FeatureActivationGuard';
|
||||
|
||||
@Service()
|
||||
export class WarehousesTransfers extends BaseController {
|
||||
@Inject()
|
||||
private warehouseTransferApplication: WarehouseTransferApplication;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[
|
||||
check('from_warehouse_id').exists().isInt().toInt(),
|
||||
check('to_warehouse_id').exists().isInt().toInt(),
|
||||
|
||||
check('date').exists().isISO8601(),
|
||||
check('transaction_number').optional(),
|
||||
|
||||
check('transfer_initiated').default(false).isBoolean().toBoolean(),
|
||||
check('transfer_delivered').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.index').exists(),
|
||||
check('entries.*.item_id').exists(),
|
||||
check('entries.*.description').optional(),
|
||||
check('entries.*.quantity').exists().isInt().toInt(),
|
||||
check('entries.*.cost').optional().isDecimal().toFloat(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.createWarehouseTransfer),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[
|
||||
param('id').exists().isInt().toInt(),
|
||||
|
||||
check('from_warehouse_id').exists().isInt().toInt(),
|
||||
check('to_warehouse_id').exists().isInt().toInt(),
|
||||
|
||||
check('date').exists().isISO8601(),
|
||||
check('transaction_number').optional(),
|
||||
|
||||
check('transfer_initiated').default(false).isBoolean().toBoolean(),
|
||||
check('transfer_delivered').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.id').optional().isInt().toInt(),
|
||||
check('entries.*.index').exists(),
|
||||
check('entries.*.item_id').exists().isInt().toInt(),
|
||||
check('entries.*.description').optional(),
|
||||
check('entries.*.quantity').exists().isInt({ min: 1 }).toInt(),
|
||||
check('entries.*.cost').optional().isDecimal().toFloat(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.editWarehouseTransfer),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.put(
|
||||
'/:id/initiate',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.asyncMiddleware(this.initiateTransfer),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.put(
|
||||
'/:id/transferred',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.asyncMiddleware(this.deliverTransfer),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getWarehousesTransfers),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getWarehouseTransfer),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteWarehouseTransfer),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new warehouse transfer transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private createWarehouseTransfer = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const createWareouseTransfer: ICreateWarehouseTransferDTO =
|
||||
this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const warehouse =
|
||||
await this.warehouseTransferApplication.createWarehouseTransfer(
|
||||
tenantId,
|
||||
createWareouseTransfer
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: warehouse.id,
|
||||
message:
|
||||
'The warehouse transfer transaction has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits warehouse transfer transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private editWarehouseTransfer = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseTransferId } = req.params;
|
||||
const editWarehouseTransferDTO: IEditWarehouseTransferDTO =
|
||||
this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const warehouseTransfer =
|
||||
await this.warehouseTransferApplication.editWarehouseTransfer(
|
||||
tenantId,
|
||||
warehouseTransferId,
|
||||
editWarehouseTransferDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: warehouseTransfer.id,
|
||||
message:
|
||||
'The warehouse transfer transaction has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given warehouse transfer transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private deleteWarehouseTransfer = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseTransferId } = req.params;
|
||||
|
||||
try {
|
||||
await this.warehouseTransferApplication.deleteWarehouseTransfer(
|
||||
tenantId,
|
||||
warehouseTransferId
|
||||
);
|
||||
return res.status(200).send({
|
||||
message:
|
||||
'The warehouse transfer transaction has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves warehouse transfer transaction details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private getWarehouseTransfer = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseTransferId } = req.params;
|
||||
|
||||
try {
|
||||
const warehouseTransfer =
|
||||
await this.warehouseTransferApplication.getWarehouseTransfer(
|
||||
tenantId,
|
||||
warehouseTransferId
|
||||
);
|
||||
return res.status(200).send({ data: warehouseTransfer });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves specific warehouse transfer transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private getWarehousesTransfers = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const filterDTO = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
try {
|
||||
const { warehousesTransfers, pagination, filter } =
|
||||
await this.warehouseTransferApplication.getWarehousesTransfers(
|
||||
tenantId,
|
||||
filterDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
data: warehousesTransfers,
|
||||
pagination,
|
||||
filter,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initiates the warehouse transfer.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private initiateTransfer = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseTransferId } = req.params;
|
||||
|
||||
try {
|
||||
await this.warehouseTransferApplication.initiateWarehouseTransfer(
|
||||
tenantId,
|
||||
warehouseTransferId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: warehouseTransferId,
|
||||
message: 'The given warehouse transfer has been initialized.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* marks the given warehouse transfer as transferred.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private deliverTransfer = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseTransferId } = req.params;
|
||||
|
||||
try {
|
||||
await this.warehouseTransferApplication.transferredWarehouseTransfer(
|
||||
tenantId,
|
||||
warehouseTransferId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: warehouseTransferId,
|
||||
message: 'The given warehouse transfer has been delivered.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'WAREHOUSES_TRANSFER_SHOULD_NOT_BE_SAME') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'WAREHOUSES_TRANSFER_SHOULD_NOT_BE_SAME', code: 100 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'FROM_WAREHOUSE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'FROM_WAREHOUSE_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TO_WAREHOUSE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TO_WAREHOUSE_NOT_FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'WAREHOUSE_TRANSFER_ITEMS_SHOULD_BE_INVENTORY') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'WAREHOUSE_TRANSFER_ITEMS_SHOULD_BE_INVENTORY', code: 500 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'WAREHOUSE_TRANSFER_ALREADY_TRANSFERRED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'WAREHOUSE_TRANSFER_ALREADY_TRANSFERRED', code: 600 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'WAREHOUSE_TRANSFER_ALREADY_INITIATED') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'WAREHOUSE_TRANSFER_ALREADY_INITIATED', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'WAREHOUSE_TRANSFER_NOT_INITIATED') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'WAREHOUSE_TRANSFER_NOT_INITIATED', code: 800 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param } from 'express-validator';
|
||||
|
||||
import { Features } from '@/interfaces';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { FeatureActivationGuard } from '@/api/middleware/FeatureActivationGuard';
|
||||
import { WarehousesApplication } from '@/services/Warehouses/WarehousesApplication';
|
||||
|
||||
@Service()
|
||||
export class WarehousesItemController extends BaseController {
|
||||
@Inject()
|
||||
warehousesApplication: WarehousesApplication;
|
||||
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/items/:id/warehouses',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.getItemWarehouses
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
getItemWarehouses = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseId } = req.params;
|
||||
|
||||
try {
|
||||
const itemWarehouses = await this.warehousesApplication.getItemWarehouses(
|
||||
tenantId,
|
||||
warehouseId
|
||||
);
|
||||
|
||||
return res.status(200).send({ itemWarehouses });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
337
packages/server/src/api/controllers/Warehouses/index.ts
Normal file
337
packages/server/src/api/controllers/Warehouses/index.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { check, param } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { WarehousesApplication } from '@/services/Warehouses/WarehousesApplication';
|
||||
import { Features, ICreateWarehouseDTO, IEditWarehouseDTO } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { FeatureActivationGuard } from '@/api/middleware/FeatureActivationGuard';
|
||||
|
||||
@Service()
|
||||
export class WarehousesController extends BaseController {
|
||||
@Inject()
|
||||
private warehouseApplication: WarehousesApplication;
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/activate',
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.activateWarehouses),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[
|
||||
check('name').exists(),
|
||||
check('code').optional({ nullable: true }),
|
||||
|
||||
check('address').optional({ nullable: true }),
|
||||
check('city').optional({ nullable: true }),
|
||||
check('country').optional({ nullable: true }),
|
||||
|
||||
check('phone_number').optional({ nullable: true }),
|
||||
check('email').optional({ nullable: true }).isEmail(),
|
||||
check('website').optional({ nullable: true }).isURL(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.createWarehouse),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[
|
||||
check('id').exists().isInt().toInt(),
|
||||
check('name').exists(),
|
||||
check('code').optional({ nullable: true }),
|
||||
|
||||
check('address').optional({ nullable: true }),
|
||||
check('city').optional({ nullable: true }),
|
||||
check('country').optional({ nullable: true }),
|
||||
|
||||
check('phone_number').optional({ nullable: true }),
|
||||
check('email').optional({ nullable: true }).isEmail(),
|
||||
check('website').optional({ nullable: true }).isURL(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.editWarehouse),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/mark-primary',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[check('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.markPrimaryWarehouse)
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteWarehouse),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getWarehouse),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
FeatureActivationGuard(Features.WAREHOUSES),
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getWarehouses),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new warehouse.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public createWarehouse = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const createWarehouseDTO: ICreateWarehouseDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const warehouse = await this.warehouseApplication.createWarehouse(
|
||||
tenantId,
|
||||
createWarehouseDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: warehouse.id,
|
||||
message: 'The warehouse has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given warehouse.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public editWarehouse = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseId } = req.params;
|
||||
const editWarehouseDTO: IEditWarehouseDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const warehouse = await this.warehouseApplication.editWarehouse(
|
||||
tenantId,
|
||||
warehouseId,
|
||||
editWarehouseDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: warehouse.id,
|
||||
message: 'The warehouse has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
public deleteWarehouse = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseId } = req.params;
|
||||
|
||||
try {
|
||||
await this.warehouseApplication.deleteWarehouse(tenantId, warehouseId);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'The warehouse has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Retrieves specific warehouse.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public getWarehouse = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseId } = req.params;
|
||||
|
||||
try {
|
||||
const warehouse = await this.warehouseApplication.getWarehouse(
|
||||
tenantId,
|
||||
warehouseId
|
||||
);
|
||||
return res.status(200).send({ warehouse });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves warehouses list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public getWarehouses = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const warehouses = await this.warehouseApplication.getWarehouses(
|
||||
tenantId
|
||||
);
|
||||
return res.status(200).send({ warehouses });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Activates multi-warehouses feature.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public activateWarehouses = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.warehouseApplication.activateWarehouses(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'The multi-warehouses has been activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks the given warehouse as primary.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public markPrimaryWarehouse = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: warehouseId } = req.params;
|
||||
|
||||
try {
|
||||
const warehouse = await this.warehouseApplication.markWarehousePrimary(
|
||||
tenantId,
|
||||
warehouseId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: warehouse.id,
|
||||
message: 'The given warehouse has been marked as primary.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'WAREHOUSE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'WAREHOUSE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MUTLI_WAREHOUSES_ALREADY_ACTIVATED') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'MUTLI_WAREHOUSES_ALREADY_ACTIVATED', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COULD_NOT_DELETE_ONLY_WAERHOUSE') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COULD_NOT_DELETE_ONLY_WAERHOUSE', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'WAREHOUSE_CODE_NOT_UNIQUE') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'WAREHOUSE_CODE_NOT_UNIQUE', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'WAREHOUSE_HAS_ASSOCIATED_TRANSACTIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'WAREHOUSE_HAS_ASSOCIATED_TRANSACTIONS', code: 500 },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
151
packages/server/src/api/index.ts
Normal file
151
packages/server/src/api/index.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Router } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
// Middlewares
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
|
||||
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
|
||||
import I18nMiddleware from '@/api/middleware/I18nMiddleware';
|
||||
import I18nAuthenticatedMiddlware from '@/api/middleware/I18nAuthenticatedMiddlware';
|
||||
import EnsureTenantIsSeeded from '@/api/middleware/EnsureTenantIsSeeded';
|
||||
|
||||
// Routes
|
||||
import Authentication from '@/api/controllers/Authentication';
|
||||
import InviteUsers from '@/api/controllers/InviteUsers';
|
||||
import Organization from '@/api/controllers/Organization';
|
||||
import Account from '@/api/controllers/Account';
|
||||
import Users from '@/api/controllers/Users';
|
||||
import Items from '@/api/controllers/Items';
|
||||
import ItemCategories from '@/api/controllers/ItemCategories';
|
||||
import Accounts from '@/api/controllers/Accounts';
|
||||
import AccountTypes from '@/api/controllers/AccountTypes';
|
||||
import Views from '@/api/controllers/Views';
|
||||
import ManualJournals from '@/api/controllers/ManualJournals';
|
||||
import FinancialStatements from '@/api/controllers/FinancialStatements';
|
||||
import Expenses from '@/api/controllers/Expenses';
|
||||
import Settings from '@/api/controllers/Settings';
|
||||
import Currencies from '@/api/controllers/Currencies';
|
||||
import Contacts from '@/api/controllers/Contacts/Contacts';
|
||||
import Customers from '@/api/controllers/Contacts/Customers';
|
||||
import Vendors from '@/api/controllers/Contacts/Vendors';
|
||||
import Sales from '@/api/controllers/Sales';
|
||||
import Purchases from '@/api/controllers/Purchases';
|
||||
import Resources from './controllers/Resources';
|
||||
import ExchangeRates from '@/api/controllers/ExchangeRates';
|
||||
import Media from '@/api/controllers/Media';
|
||||
import Ping from '@/api/controllers/Ping';
|
||||
import Subscription from '@/api/controllers/Subscription';
|
||||
import Licenses from '@/api/controllers/Subscription/Licenses';
|
||||
import InventoryAdjustments from '@/api/controllers/Inventory/InventoryAdjustments';
|
||||
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
|
||||
import Jobs from './controllers/Jobs';
|
||||
import Miscellaneous from '@/api/controllers/Miscellaneous';
|
||||
import OrganizationDashboard from '@/api/controllers/OrganizationDashboard';
|
||||
import CashflowController from './controllers/Cashflow/CashflowController';
|
||||
import AuthorizationMiddleware from './middleware/AuthorizationMiddleware';
|
||||
import RolesController from './controllers/Roles';
|
||||
import TransactionsLocking from './controllers/TransactionsLocking';
|
||||
import DashboardController from './controllers/Dashboard';
|
||||
import { BranchesController } from './controllers/Branches';
|
||||
import { WarehousesController } from './controllers/Warehouses';
|
||||
import { WarehousesTransfers } from './controllers/Warehouses/WarehouseTransfers';
|
||||
import { WarehousesItemController } from './controllers/Warehouses/WarehousesItem';
|
||||
import { BranchIntegrationErrorsMiddleware } from '@/services/Branches/BranchIntegrationErrorsMiddleware';
|
||||
import { InventoryItemsCostController } from './controllers/Inventory/InventortyItemsCosts';
|
||||
import { ProjectsController } from './controllers/Projects/Projects';
|
||||
import { ProjectTasksController } from './controllers/Projects/Tasks';
|
||||
import { ProjectTimesController } from './controllers/Projects/Times';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
|
||||
// - Global routes.
|
||||
// ---------------------------
|
||||
app.use(asyncRenderMiddleware);
|
||||
app.use(I18nMiddleware);
|
||||
|
||||
app.use('/auth', Container.get(Authentication).router());
|
||||
app.use('/invite', Container.get(InviteUsers).nonAuthRouter());
|
||||
app.use('/licenses', Container.get(Licenses).router());
|
||||
app.use('/subscription', Container.get(Subscription).router());
|
||||
app.use('/organization', Container.get(Organization).router());
|
||||
app.use('/ping', Container.get(Ping).router());
|
||||
app.use('/jobs', Container.get(Jobs).router());
|
||||
app.use('/account', Container.get(Account).router());
|
||||
|
||||
// - Dashboard routes.
|
||||
// ---------------------------
|
||||
const dashboard = Router();
|
||||
|
||||
dashboard.use(JWTAuth);
|
||||
dashboard.use(AttachCurrentTenantUser);
|
||||
dashboard.use(TenancyMiddleware);
|
||||
dashboard.use(SubscriptionMiddleware('main'));
|
||||
dashboard.use(EnsureTenantIsInitialized);
|
||||
dashboard.use(SettingsMiddleware);
|
||||
dashboard.use(I18nAuthenticatedMiddlware);
|
||||
dashboard.use(EnsureTenantIsSeeded);
|
||||
dashboard.use(AuthorizationMiddleware);
|
||||
|
||||
dashboard.use('/organization', Container.get(OrganizationDashboard).router());
|
||||
dashboard.use('/users', Container.get(Users).router());
|
||||
dashboard.use('/invite', Container.get(InviteUsers).authRouter());
|
||||
dashboard.use('/currencies', Container.get(Currencies).router());
|
||||
dashboard.use('/settings', Container.get(Settings).router());
|
||||
dashboard.use('/accounts', Container.get(Accounts).router());
|
||||
dashboard.use('/account_types', Container.get(AccountTypes).router());
|
||||
dashboard.use('/manual-journals', Container.get(ManualJournals).router());
|
||||
dashboard.use('/views', Container.get(Views).router());
|
||||
dashboard.use('/items', Container.get(Items).router());
|
||||
dashboard.use('/item_categories', Container.get(ItemCategories).router());
|
||||
dashboard.use('/expenses', Container.get(Expenses).router());
|
||||
dashboard.use(
|
||||
'/financial_statements',
|
||||
Container.get(FinancialStatements).router()
|
||||
);
|
||||
dashboard.use('/contacts', Container.get(Contacts).router());
|
||||
dashboard.use('/customers', Container.get(Customers).router());
|
||||
dashboard.use('/vendors', Container.get(Vendors).router());
|
||||
dashboard.use('/sales', Container.get(Sales).router());
|
||||
dashboard.use('/purchases', Container.get(Purchases).router());
|
||||
dashboard.use('/resources', Container.get(Resources).router());
|
||||
dashboard.use('/exchange_rates', Container.get(ExchangeRates).router());
|
||||
dashboard.use('/media', Container.get(Media).router());
|
||||
dashboard.use(
|
||||
'/inventory_adjustments',
|
||||
Container.get(InventoryAdjustments).router()
|
||||
);
|
||||
dashboard.use(
|
||||
'/inventory',
|
||||
Container.get(InventoryItemsCostController).router()
|
||||
);
|
||||
dashboard.use('/cashflow', Container.get(CashflowController).router());
|
||||
dashboard.use('/roles', Container.get(RolesController).router());
|
||||
dashboard.use(
|
||||
'/transactions-locking',
|
||||
Container.get(TransactionsLocking).router()
|
||||
);
|
||||
dashboard.use('/branches', Container.get(BranchesController).router());
|
||||
dashboard.use(
|
||||
'/warehouses/transfers',
|
||||
Container.get(WarehousesTransfers).router()
|
||||
);
|
||||
dashboard.use('/warehouses', Container.get(WarehousesController).router());
|
||||
dashboard.use('/projects', Container.get(ProjectsController).router());
|
||||
dashboard.use('/', Container.get(ProjectTasksController).router());
|
||||
dashboard.use('/', Container.get(ProjectTimesController).router());
|
||||
|
||||
dashboard.use('/', Container.get(WarehousesItemController).router());
|
||||
|
||||
dashboard.use('/dashboard', Container.get(DashboardController).router());
|
||||
dashboard.use('/', Container.get(Miscellaneous).router());
|
||||
|
||||
app.use('/', dashboard);
|
||||
|
||||
app.use(BranchIntegrationErrorsMiddleware);
|
||||
|
||||
return app;
|
||||
};
|
||||
23
packages/server/src/api/middleware/AsyncRenderMiddleware.ts
Normal file
23
packages/server/src/api/middleware/AsyncRenderMiddleware.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
const asyncRender = (app) => (path: string, attributes = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
app.render(path, attributes, (error, data) => {
|
||||
if (error) { reject(error); }
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Injects `asyncRender` method to response object.
|
||||
* @param {Request} req Express req Object
|
||||
* @param {Response} res Express res Object
|
||||
* @param {NextFunction} next Express next Function
|
||||
*/
|
||||
const asyncRenderMiddleware = (req: Request, res: Response, next: Function) => {
|
||||
res.asyncRender = asyncRender(req.app);
|
||||
next();
|
||||
};
|
||||
|
||||
export default asyncRenderMiddleware;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Container } from 'typedi';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
/**
|
||||
* Attach user to req.currentUser
|
||||
* @param {Request} req Express req Object
|
||||
* @param {Response} res Express res Object
|
||||
* @param {NextFunction} next Express next Function
|
||||
*/
|
||||
const attachCurrentUser = async (req: Request, res: Response, next: Function) => {
|
||||
const Logger = Container.get('logger');
|
||||
const { systemUserRepository } = Container.get('repositories');
|
||||
|
||||
try {
|
||||
Logger.info('[attach_user_middleware] finding system user by id.');
|
||||
const user = await systemUserRepository.findOneById(req.token.id);
|
||||
|
||||
if (!user) {
|
||||
Logger.info('[attach_user_middleware] the system user not found.');
|
||||
return res.boom.unauthorized();
|
||||
}
|
||||
if (!user.active) {
|
||||
Logger.info('[attach_user_middleware] the system user not found.');
|
||||
return res.boom.badRequest(
|
||||
'The authorized user is inactivated.',
|
||||
{ errors: [{ type: 'USER_INACTIVE', code: 100, }] },
|
||||
);
|
||||
}
|
||||
// Delete password property from user object.
|
||||
Reflect.deleteProperty(user, 'password');
|
||||
req.user = user;
|
||||
return next();
|
||||
} catch (e) {
|
||||
Logger.error('[attach_user_middleware] error attaching user to req: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
};
|
||||
|
||||
export default attachCurrentUser;
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Ability } from '@casl/ability';
|
||||
import LruCache from 'lru-cache';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { IRole, IRolePremission, ISystemUser } from '@/interfaces';
|
||||
|
||||
// store abilities of 1000 most active users
|
||||
export const ABILITIES_CACHE = new LruCache(1000);
|
||||
|
||||
/**
|
||||
* Retrieve ability for the given role.
|
||||
* @param {} role
|
||||
* @returns
|
||||
*/
|
||||
function getAbilityForRole(role) {
|
||||
const rules = getAbilitiesRolesConds(role);
|
||||
return new Ability(rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve abilities of the given role.
|
||||
* @param {IRole} role
|
||||
* @returns {}
|
||||
*/
|
||||
function getAbilitiesRolesConds(role: IRole) {
|
||||
switch (role.slug) {
|
||||
case 'admin': // predefined role.
|
||||
return getSuperAdminRules();
|
||||
default:
|
||||
return getRulesFromRolePermissions(role.permissions || []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the super admin rules.
|
||||
* @returns {}
|
||||
*/
|
||||
function getSuperAdminRules() {
|
||||
return [{ action: 'manage', subject: 'all' }];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve CASL rules from role permissions.
|
||||
* @param {IRolePremission[]} permissions -
|
||||
* @returns {}
|
||||
*/
|
||||
function getRulesFromRolePermissions(permissions: IRolePremission[]) {
|
||||
return permissions
|
||||
.filter((permission: IRolePremission) => permission.value)
|
||||
.map((permission: IRolePremission) => {
|
||||
return {
|
||||
action: permission.ability,
|
||||
subject: permission.subject,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve ability for user.
|
||||
* @param {ISystemUser} user
|
||||
* @param {number} tenantId
|
||||
* @returns {}
|
||||
*/
|
||||
async function getAbilityForUser(user: ISystemUser, tenantId: number) {
|
||||
const tenancy = Container.get(HasTenancyService);
|
||||
const { User } = tenancy.models(tenantId);
|
||||
|
||||
const tenantUser = await User.query()
|
||||
.findOne('systemUserId', user.id)
|
||||
.withGraphFetched('role.permissions');
|
||||
|
||||
return getAbilityForRole(tenantUser.role);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Request} request -
|
||||
* @param {Response} response -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
export default async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { tenantId, user } = req;
|
||||
|
||||
if (ABILITIES_CACHE.has(req.user.id)) {
|
||||
req.ability = ABILITIES_CACHE.get(req.user.id);
|
||||
} else {
|
||||
req.ability = await getAbilityForUser(req.user, tenantId);
|
||||
ABILITIES_CACHE.set(req.user.id, req.ability);
|
||||
}
|
||||
next();
|
||||
};
|
||||
18
packages/server/src/api/middleware/CheckPolicies.ts
Normal file
18
packages/server/src/api/middleware/CheckPolicies.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ForbiddenError } from '@casl/ability';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default (ability: string, subject: string) =>
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
ForbiddenError.from(req.ability).throwUnlessCan(ability, subject);
|
||||
} catch (error) {
|
||||
return res.status(403).send({
|
||||
type: 'USER_PERMISSIONS_FORBIDDEN',
|
||||
message: `You are not allowed to ${error.action} on ${error.subjectType}`,
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import deepMap from 'deep-map';
|
||||
import { convertEmptyStringToNull } from 'utils';
|
||||
|
||||
function convertEmptyStringsToNull(data) {
|
||||
return deepMap(data, (value) => convertEmptyStringToNull(value));
|
||||
}
|
||||
|
||||
export default (req: Request, res: Response, next: NextFunction) => {
|
||||
const transfomedBody = convertEmptyStringsToNull(req.body);
|
||||
req.body = transfomedBody;
|
||||
next();
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Container } from 'typedi';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
|
||||
export default (req: Request, res: Response, next: Function) => {
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
if (!req.tenant) {
|
||||
Logger.info('[ensure_tenant_intialized_middleware] no tenant model.');
|
||||
throw new Error('Should load this middleware after `TenancyMiddleware`.');
|
||||
}
|
||||
if (!req.tenant.initializedAt) {
|
||||
Logger.info('[ensure_tenant_initialized_middleware] tenant database not initalized.');
|
||||
|
||||
return res.boom.badRequest(
|
||||
'Tenant database is not migrated with application schema yut.',
|
||||
{ errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }] },
|
||||
);
|
||||
}
|
||||
next();
|
||||
};
|
||||
21
packages/server/src/api/middleware/EnsureTenantIsSeeded.ts
Normal file
21
packages/server/src/api/middleware/EnsureTenantIsSeeded.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Container } from 'typedi';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export default (req: Request, res: Response, next: Function) => {
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
if (!req.tenant) {
|
||||
Logger.info('[ensure_tenant_intialized_middleware] no tenant model.');
|
||||
throw new Error('Should load this middleware after `TenancyMiddleware`.');
|
||||
}
|
||||
if (!req.tenant.seededAt) {
|
||||
Logger.info(
|
||||
'[ensure_tenant_initialized_middleware] tenant databae not seeded.'
|
||||
);
|
||||
return res.boom.badRequest(
|
||||
'Tenant database is not seeded with initial data yet.',
|
||||
{ errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }] }
|
||||
);
|
||||
}
|
||||
next();
|
||||
};
|
||||
18
packages/server/src/api/middleware/FeatureActivationGuard.ts
Normal file
18
packages/server/src/api/middleware/FeatureActivationGuard.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Features } from '@/interfaces';
|
||||
|
||||
export const FeatureActivationGuard =
|
||||
(feature: Features) => (req: Request, res: Response, next: Function) => {
|
||||
const { settings } = req;
|
||||
|
||||
const isActivated = settings.get({ group: 'features', key: feature });
|
||||
|
||||
if (!isActivated) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'FEATURE_NOT_ACTIVATED', code: 20, payload: { feature } },
|
||||
],
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user