add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View 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);
}
};
}

View 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);
}
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
};
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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(),
];
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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),
};
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
};
}

View File

@@ -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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
};
}

View 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;
}
}

View 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);
};
}

View 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);
};
}

View 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);
}
};

View 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);
}
}
}

View 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);
}
}

View 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);
};
}

View 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,
});
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View 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;
}
}

View 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);
}
}

View File

@@ -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);
}
};
}

View 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);
}
};
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -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);
}
}
}

View 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 });
}
}

View File

@@ -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);
};
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View 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);
}
}
}

View 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);
};
}

View File

@@ -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();
};

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
};
}

View 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);
}
}

View 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;
};

View 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;

View File

@@ -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;

View File

@@ -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();
};

View 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();
};

View File

@@ -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();
};

View 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.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();
};

View 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();
};

View 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