feat: Payment system with voucher cards.

feat: Design with inversion dependency injection architecture.
feat: Prettier http middleware.
feat: Re-write items categories with preferred accounts.
This commit is contained in:
Ahmed Bouhuolia
2020-08-27 20:39:55 +02:00
parent e23b8d9947
commit e4270dc039
63 changed files with 2567 additions and 462 deletions

View File

@@ -1,327 +0,0 @@
import express from 'express';
import {
check,
param,
validationResult,
query,
} from 'express-validator';
import { difference } from 'lodash';
import asyncMiddleware from '../middleware/asyncMiddleware';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter';
import {
mapFilterRolesToDynamicFilter,
} from '@/lib/ViewRolesBuilder';
export default {
/**
* Router constructor method.
*/
router() {
const router = express.Router();
// const permit = Authorization('items_categories');
router.post('/:id',
this.editCategory.validation,
asyncMiddleware(this.editCategory.handler));
router.post('/',
this.newCategory.validation,
asyncMiddleware(this.newCategory.handler));
router.delete('/bulk',
this.bulkDeleteCategories.validation,
asyncMiddleware(this.bulkDeleteCategories.handler));
router.delete('/:id',
this.deleteItem.validation,
asyncMiddleware(this.deleteItem.handler));
router.get('/:id',
this.getCategory.validation,
asyncMiddleware(this.getCategory.handler));
router.get('/',
this.getList.validation,
asyncMiddleware(this.getList.handler));
return router;
},
/**
* Creates a new item category.
*/
newCategory: {
validation: [
check('name').exists().trim().escape(),
check('parent_category_id')
.optional({ nullable: true, checkFalsy: true })
.isNumeric()
.toInt(),
check('description')
.optional()
.trim()
.escape(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { user } = req;
const form = { ...req.body };
const { ItemCategory } = req.models;
if (form.parent_category_id) {
const foundParentCategory = await ItemCategory.query()
.where('id', form.parent_category_id)
.first();
if (!foundParentCategory) {
return res.boom.notFound('The parent category ID is not found.', {
errors: [{ type: 'PARENT_CATEGORY_NOT_FOUND', code: 100 }],
});
}
}
const category = await ItemCategory.query().insert({
...form,
user_id: user.id,
});
return res.status(200).send({ category });
},
},
/**
* Edit details of the given category item.
*/
editCategory: {
validation: [
param('id').toInt(),
check('name').exists().trim().escape(),
check('parent_category_id')
.optional({ nullable: true, checkFalsy: true })
.isNumeric()
.toInt(),
check('description').optional().trim().escape(),
],
async handler(req, res) {
const { id } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error',
...validationErrors,
});
}
const form = { ...req.body };
const { ItemCategory } = req.models;
const itemCategory = await ItemCategory.query()
.where('id', id)
.first();
if (!itemCategory) {
return res.boom.notFound({
errors: [{ type: 'ITEM_CATEGORY.NOT.FOUND', code: 100 }],
});
}
if (
form.parent_category_id
&& form.parent_category_id !== itemCategory.parent_category_id
) {
const foundParentCategory = await ItemCategory.query()
.where('id', form.parent_category_id)
.first();
if (!foundParentCategory) {
return res.boom.notFound('The parent category ID is not found.', {
errors: [{ type: 'PARENT_CATEGORY_NOT_FOUND', code: 100 }],
});
}
}
const updateItemCategory = await ItemCategory.query()
.where('id', id)
.update({ ...form });
return res.status(200).send({ id });
},
},
/**
* Delete the give item category.
*/
deleteItem: {
validation: [
param('id').exists().toInt(),
],
async handler(req, res) {
const { id } = req.params;
const { ItemCategory } = req.models;
const itemCategory = await ItemCategory.query()
.where('id', id)
.first();
if (!itemCategory) {
return res.boom.notFound();
}
await ItemCategory.query()
.where('id', itemCategory.id)
.delete();
return res.status(200).send();
},
},
/**
* Retrieve the list of items.
*/
getList: {
validation: [
query('column_sort_order').optional().trim().escape(),
query('sort_order').optional().trim().escape().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { Resource, ItemCategory } = req.models;
const categoriesResource = await Resource.query()
.where('name', 'items_categories')
.withGraphFetched('fields')
.first();
if (!categoriesResource) {
return res.status(400).send({
errors: [{ type: 'ITEMS.CATEGORIES.RESOURCE.NOT.FOUND', code: 200 }],
});
}
const filter = {
column_sort_order: '',
sort_order: '',
filter_roles: [],
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const errorReasons = [];
const resourceFieldsKeys = categoriesResource.fields.map((c) => c.key);
const dynamicFilter = new DynamicFilter(ItemCategory.tableName);
// Dynamic filter with filter roles.
if (filter.filter_roles.length > 0) {
// Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles),
categoriesResource.fields,
);
categoriesResource.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ITEMS.RESOURCE.HAS.NO.FIELDS', code: 500 });
}
}
// Dynamic filter with column sort order.
if (filter.column_sort_order) {
if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) {
errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
}
const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_order,
filter.sort_order,
);
dynamicFilter.setFilter(sortByFilter);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const categories = await ItemCategory.query().onBuild((builder) => {
dynamicFilter.buildQuery()(builder);
builder.select([
'*',
ItemCategory.relatedQuery('items').count().as('count'),
]);
});
return res.status(200).send({ categories });
},
},
/**
* Retrieve details of the given category.
*/
getCategory: {
validation: [param('category_id').toInt()],
async handler(req, res) {
const { category_id: categoryId } = req.params;
const { ItemCategory } = req.models;
const item = await ItemCategory.where('id', categoryId).fetch();
if (!item) {
return res.boom.notFound(null, {
errors: [{ type: 'CATEGORY_NOT_FOUND', code: 100 }],
});
}
return res.status(200).send({ category: item.toJSON() });
},
},
/**
* Bulk delete the given item categories.
*/
bulkDeleteCategories: {
validation: [
query('ids').isArray({ min: 2 }),
query('ids.*').isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const filter = {
ids: [],
...req.query,
};
const { ItemCategory } = req.models;
const itemCategories = await ItemCategory.query().whereIn('id', filter.ids);
const itemCategoriesIds = itemCategories.map((category) => category.id);
const notFoundCategories = difference(filter.ids, itemCategoriesIds);
if (notFoundCategories.length > 0) {
return res.status(400).send({
errors: [{ type: 'ITEM.CATEGORIES.IDS.NOT.FOUND', code: 200 }],
});
}
await ItemCategory.query().whereIn('id', filter.ids).delete();
return res.status(200).send({ ids: filter.ids });
},
},
};

View File

@@ -0,0 +1,437 @@
import express from 'express';
import {
check,
param,
query,
} from 'express-validator';
import { difference } from 'lodash';
import { Service } from 'typedi';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter';
import {
mapFilterRolesToDynamicFilter,
} from '@/lib/ViewRolesBuilder';
import { IItemCategory, IItemCategoryOTD } from '@/interfaces';
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
@Service()
export default class ItemsCategoriesController {
/**
* Router constructor method.
*/
constructor() {
const router = express.Router();
router.post('/:id', [
...this.categoryValidationSchema,
...this.specificCategoryValidationSchema,
],
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateParentCategoryExistance),
asyncMiddleware(this.validateSellAccountExistance),
asyncMiddleware(this.validateCostAccountExistance),
asyncMiddleware(this.validateInventoryAccountExistance),
asyncMiddleware(this.editCategory)
);
router.post('/',
this.categoryValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateParentCategoryExistance),
asyncMiddleware(this.validateSellAccountExistance),
asyncMiddleware(this.validateCostAccountExistance),
asyncMiddleware(this.validateInventoryAccountExistance),
asyncMiddleware(this.newCategory),
);
router.delete('/bulk',
this.categoriesBulkValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateCategoriesIdsExistance),
asyncMiddleware(this.bulkDeleteCategories),
);
router.delete('/:id',
this.specificCategoryValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateItemCategoryExistance),
asyncMiddleware(this.deleteItem),
);
router.get('/:id',
this.specificCategoryValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateItemCategoryExistance),
asyncMiddleware(this.getCategory)
);
router.get('/',
this.categoriesListValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.getList)
);
return router;
}
/**
* Item category validation schema.
*/
get categoryValidationSchema() {
return [
check('name').exists().trim().escape(),
check('parent_category_id')
.optional({ nullable: true, checkFalsy: true })
.isNumeric()
.toInt(),
check('description')
.optional()
.trim()
.escape(),
check('sell_account_id')
.optional({ nullable: true, checkFalsy: true })
.isNumeric()
.toInt(),
check('cost_account_id')
.optional()
.isNumeric()
.toInt(),
check('inventory_account_id')
.optional()
.isNumeric()
.toInt(),
]
}
/**
* Validate items categories bulk actions.
*/
get categoriesBulkValidationSchema() {
return [
query('ids').isArray({ min: 2 }),
query('ids.*').isNumeric().toInt(),
];
}
/**
* Validate items categories schema.
*/
get categoriesListValidationSchema() {
return [
query('column_sort_order').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(),
];
}
/**
* Validate the item category existance.
* @param {Request} req
* @param {Response} res
*/
async validateItemCategoryExistance(req: Request, res: Response, next: Function) {
const categoryId: number = req.params.id;
const { ItemCategory } = req.models;
const category = await ItemCategory.query().findById(categoryId);
if (!category) {
return res.boom.notFound(null, {
errors: [{ type: 'ITEM_CATEGORY_NOT_FOUND', code: 100 }],
});
}
next();
}
/**
* Validate wether the given cost account exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateCostAccountExistance(req: Request, res: Response, next: Function) {
const { Account, AccountType } = req.models;
const category: IItemCategoryOTD = { ...req.body };
if (category.costAccountId) {
const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold');
const foundAccount = await Account.query().findById(category.costAccountId)
if (!foundAccount) {
return res.status(400).send({
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
});
} else if (foundAccount.accountTypeId !== COGSType.id) {
return res.status(400).send({
errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }],
});
}
}
next();
}
/**
* Validate wether the given sell account exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async validateSellAccountExistance(req: Request, res: Response, next: Function) {
const { Account, AccountType } = req.models;
const category: IItemCategoryOTD = { ...req.body };
if (category.sellAccountId) {
const incomeType = await AccountType.query().findOne('key', 'income');
const foundAccount = await Account.query().findById(category.sellAccountId);
if (!foundAccount) {
return res.status(400).send({
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
});
} else if (foundAccount.accountTypeId !== incomeType.id) {
return res.status(400).send({
errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }],
})
}
}
next();
}
/**
* Validates wether the given inventory account exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async validateInventoryAccountExistance(req: Request, res: Response, next: Function) {
const { Account, AccountType } = req.models;
const category: IItemCategoryOTD = { ...req.body };
if (category.inventoryAccountId) {
const otherAsset = await AccountType.query().findOne('key', 'other_asset');
const foundAccount = await Account.query().findById(category.inventoryAccountId);
if (!foundAccount) {
return res.status(400).send({
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200}],
});
} else if (otherAsset.id !== foundAccount.accountTypeId) {
return res.status(400).send({
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }],
});
}
}
next();
}
/**
* Validate the item category parent category whether exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateParentCategoryExistance(req: Request, res: Response, next: Function) {
const category: IItemCategory = { ...req.body };
const { ItemCategory } = req.models;
if (category.parentCategoryId) {
const foundParentCategory = await ItemCategory.query()
.where('id', category.parentCategoryId)
.first();
if (!foundParentCategory) {
return res.boom.notFound('The parent category ID is not found.', {
errors: [{ type: 'PARENT_CATEGORY_NOT_FOUND', code: 100 }],
});
}
}
next();
}
/**
* Validate item categories ids existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateCategoriesIdsExistance(req: Request, res: Response, next: Function) {
const ids: number[] = (req.query?.ids || []);
const { ItemCategory } = req.models;
const itemCategories = await ItemCategory.query().whereIn('id', ids);
const itemCategoriesIds = itemCategories.map((category: IItemCategory) => category.id);
const notFoundCategories = difference(ids, itemCategoriesIds);
if (notFoundCategories.length > 0) {
return res.status(400).send({
errors: [{ type: 'ITEM.CATEGORIES.IDS.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Creates a new item category.
* @param {Request} req
* @param {Response} res
*/
async newCategory(req: Request, res: Response) {
const { user } = req;
const category: IItemCategory = { ...req.body };
const { ItemCategory } = req.models;
const storedCategory = await ItemCategory.query().insert({
...category,
user_id: user.id,
});
return res.status(200).send({ category: storedCategory });
}
/**
* Edit details of the given category item.
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async editCategory(req: Request, res: Response) {
const { id } = req.params;
const category: IItemCategory = { ...req.body };
const { ItemCategory } = req.models;
const updateItemCategory = await ItemCategory.query()
.where('id', id)
.update({ ...category });
return res.status(200).send({ id });
}
/**
* Delete the give item category.
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async deleteItem(req: Request, res: Response) {
const { id } = req.params;
const { ItemCategory } = req.models;
await ItemCategory.query()
.where('id', id)
.delete();
return res.status(200).send({ id });
}
/**
* Retrieve the list of items.
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async getList(req: Request, res: Response) {
const { Resource, ItemCategory } = req.models;
const categoriesResource = await Resource.query()
.where('name', 'items_categories')
.withGraphFetched('fields')
.first();
if (!categoriesResource) {
return res.status(400).send({
errors: [{ type: 'ITEMS.CATEGORIES.RESOURCE.NOT.FOUND', code: 200 }],
});
}
const filter = {
column_sort_order: '',
sort_order: '',
filter_roles: [],
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const errorReasons = [];
const resourceFieldsKeys = categoriesResource.fields.map((c) => c.key);
const dynamicFilter = new DynamicFilter(ItemCategory.tableName);
// Dynamic filter with filter roles.
if (filter.filter_roles.length > 0) {
// Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles),
categoriesResource.fields,
);
categoriesResource.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ITEMS.RESOURCE.HAS.NO.FIELDS', code: 500 });
}
}
// Dynamic filter with column sort order.
if (filter.column_sort_order) {
if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) {
errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
}
const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_order,
filter.sort_order,
);
dynamicFilter.setFilter(sortByFilter);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const categories = await ItemCategory.query().onBuild((builder) => {
dynamicFilter.buildQuery()(builder);
builder.select([
'*',
ItemCategory.relatedQuery('items').count().as('count'),
]);
});
return res.status(200).send({ categories });
}
/**
* Retrieve details of the given category.
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async getCategory(req: Request, res: Response) {
const itemCategoryId: number = req.params.id;
const { ItemCategory } = req.models;
const itemCategory = await ItemCategory.query().findById(itemCategoryId);
return res.status(200).send({ category: itemCategory });
}
/**
* Bulk delete the given item categories.
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async bulkDeleteCategories(req: Request, res: Response) {
const ids = req.query.ids;
const { ItemCategory } = req.models;
await ItemCategory.query().whereIn('id', ids).delete();
return res.status(200).send({ ids: filter.ids });
}
};

View File

@@ -1,11 +1,11 @@
import 'reflect-metadata';
import { Router, Request, Response } from 'express';
import { Container } from 'typedi';
export default class Ping {
/**
* Router constur
*/
static router() {
router() {
const router = Router();
router.get(
@@ -20,7 +20,7 @@ export default class Ping {
* @param {Request} req
* @param {Response} res
*/
static async ping(req: Request, res: Response)
async ping(req: Request, res: Response)
{
return res.status(200).send({
server: true,

View File

@@ -0,0 +1,29 @@
import { Inject } from 'typedi';
import { Plan } from '@/system/models';
import SubscriptionService from '@/services/Subscription/SubscriptionService';
export default class PaymentMethodController {
@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 } = req.body;
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,118 @@
import { Container, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { check, param, query, ValidationSchema } from 'express-validator';
import { Voucher, Plan } from '@/system/models';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import PaymentMethodController from '@/http/controllers/Subscription/PaymentMethod';
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
import {
NotAllowedChangeSubscriptionPlan
} from '@/exceptions';
@Service()
export default class PaymentViaVoucherController extends PaymentMethodController {
/**
* Router constructor.
*/
router() {
const router = Router();
router.post(
'/payment',
this.paymentViaVoucherSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateVoucherCodeExistance.bind(this)),
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
asyncMiddleware(this.validateVoucherAndPlan.bind(this)),
asyncMiddleware(this.paymentViaVoucher.bind(this)),
);
return router;
}
/**
* Payment via voucher validation schema.
*/
get paymentViaVoucherSchema() {
return [
check('plan_slug').exists().trim().escape(),
check('voucher_code').exists().trim().escape(),
];
}
/**
* Validate the given voucher code exists on the storage.
* @async
* @param {Request} req
* @param {Response} res
*/
async validateVoucherCodeExistance(req: Request, res: Response, next: Function) {
const { voucherCode } = req.body;
const foundVoucher = await Voucher.query()
.modify('filterActiveVoucher')
.where('voucher_code', voucherCode)
.first();
if (!foundVoucher) {
return res.status(400).send({
errors: [{ type: 'VOUCHER.CODE.IS.INVALID', code: 120 }],
});
}
next();
}
/**
* Validate the voucher period and plan period.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateVoucherAndPlan(req: Request, res: Response, next: Function) {
const { planSlug, voucherCode } = req.body;
const voucher = await Voucher.query().findOne('voucher_code', voucherCode);
const plan = await Plan.query().findOne('slug', planSlug);
if (voucher.planId !== plan.id) {
return res.status(400).send({
errors: [{ type: 'VOUCHER.NOT.FOR.GIVEN.PLAN' }],
});
}
next();
}
/**
* Handle the subscription payment via voucher code.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async paymentViaVoucher(req: Request, res: Response, next: Function) {
const { planSlug, voucherCode } = req.body;
const { tenant } = req;
try {
await this.subscriptionService.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
return res.status(200).send({
type: 'PAYMENT.SUCCESSFULLY.MADE',
code: 100,
});
} catch (exception) {
const errorReasons = [];
if (exception.name === 'NotAllowedChangeSubscriptionPlan') {
errorReasons.push({
type: 'NOT.ALLOWED.RENEW.SUBSCRIPTION.WHILE.ACTIVE',
code: 120,
});
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
next(exception);
}
}
}

View File

@@ -0,0 +1,261 @@
import { Router, Request, Response } from 'express'
import { repeat, times, orderBy } from 'lodash';
import { check, oneOf, param, query, ValidationChain } from 'express-validator';
import { Container, Service, Inject } from 'typedi';
import { Voucher, Plan } from '@/system/models';
import VoucherService from '@/services/Payment/Voucher';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import PrettierMiddleware from '@/http/middleware/prettierMiddleware';
import { IVouchersFilter } from '@/interfaces';
@Service()
export default class VouchersController {
@Inject()
voucherService: VoucherService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.post(
'/generate',
this.generateVoucherSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validatePlanExistance),
asyncMiddleware(this.generateVoucher.bind(this)),
);
router.post(
'/disable/:voucherId',
PrettierMiddleware,
asyncMiddleware(this.validateVoucherExistance),
asyncMiddleware(this.validateNotDisabledVoucher),
asyncMiddleware(this.disableVoucher.bind(this)),
);
router.post(
'/send',
this.sendVoucherSchemaValidation,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.sendVoucher.bind(this)),
);
router.delete(
'/:voucherId',
PrettierMiddleware,
asyncMiddleware(this.validateVoucherExistance),
asyncMiddleware(this.deleteVoucher.bind(this)),
);
router.get(
'/',
PrettierMiddleware,
asyncMiddleware(this.listVouchers.bind(this)),
);
return router;
}
/**
* Generate voucher validation schema.
*/
get generateVoucherSchema(): 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_id').exists().isNumeric().toInt(),
];
}
/**
* Specific voucher validation schema.
*/
get specificVoucherSchema(): ValidationChain[] {
return [
oneOf([
check('voucher_id').exists().isNumeric().toInt(),
], [
check('voucher_code').exists().isNumeric().toInt(),
])
]
}
/**
* Send voucher validation schema.
*/
get sendVoucherSchemaValidation(): ValidationChain[] {
return [
check('period').exists().isNumeric(),
check('period_interval').exists().trim().escape(),
check('plan_id').exists().isNumeric().toInt(),
oneOf([
check('phone_number').exists().trim().escape(),
check('email').exists().trim().escape(),
]),
];
}
/**
* Validate the plan existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validatePlanExistance(req: Request, res: Response, next: Function) {
const planId: number = req.body.planId || req.params.planId;
const foundPlan = await Plan.query().findById(planId);
if (!foundPlan) {
return res.status(400).send({
erorrs: [{ type: 'PLAN.NOT.FOUND', code: 100 }],
});
}
next();
}
/**
* Valdiate the voucher existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function}
*/
async validateVoucherExistance(req: Request, res: Response, next: Function) {
const voucherId = req.body.voucherId || req.params.voucherId;
const foundVoucher = await Voucher.query().findById(voucherId);
if (!foundVoucher) {
return res.status(400).send({
errors: [{ type: 'VOUCHER.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Validates whether the voucher id is disabled.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateNotDisabledVoucher(req: Request, res: Response, next: Function) {
const voucherId = req.params.voucherId || req.query.voucherId;
const foundVoucher = await Voucher.query().findById(voucherId);
if (foundVoucher.disabled) {
return res.status(400).send({
errors: [{ type: 'VOUCHER.ALREADY.DISABLED', code: 200 }],
});
}
next();
}
/**
* Generate vouchers codes with given period in bulk.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async generateVoucher(req: Request, res: Response) {
const { loop = 10, period, periodInterval, planId } = req.body;
const generatedVouchers: string[] = [];
const asyncOpers = [];
times(loop, () => {
const generateOper = this.voucherService
.generateVoucher(period, periodInterval, planId)
.then((generatedVoucher: any) => {
generatedVouchers.push(generatedVoucher)
});
asyncOpers.push(generateOper);
});
return res.status(200).send({
vouchers: generatedVouchers,
});
}
/**
* Disable the given voucher on the storage.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async disableVoucher(req: Request, res: Response) {
const { voucherId } = req.params;
await this.voucherService.disableVoucher(voucherId);
return res.status(200).send({ voucher_id: voucherId });
}
/**
* Deletes the given voucher code on the storage.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async deleteVoucher(req: Request, res: Response) {
const { voucherId } = req.params;
await this.voucherService.deleteVoucher(voucherId);
return res.status(200).send({ voucher_id: voucherId });
}
/**
* Send voucher code in the given period to the customer via email or phone number
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async sendVoucher(req: Request, res: Response) {
const { phoneNumber, email, period, periodInterval, planId } = req.body;
const voucher = await Voucher.query()
.modify('filterActiveVoucher')
.where('voucher_period', period)
.where('period_interval', periodInterval)
.where('plan_id', planId)
.first();
if (!voucher) {
return res.status(400).send({
status: 110,
message: 'There is no vouchers availiable right now with the given period and plan.',
code: 'NO.AVALIABLE.VOUCHER.CODE',
});
}
await this.voucherService.sendVoucherToCustomer(
voucher.voucherCode, phoneNumber, email,
);
return res.status(200).send({
status: 100,
code: 'VOUCHER.CODE.SENT',
message: 'The voucher has been sent to the given customer.',
});
}
/**
* Listing vouchers.
* @param {Request} req
* @param {Response} res
*/
async listVouchers(req: Request, res: Response) {
const filter: IVouchersFilter = {
disabled: false,
used: false,
sent: false,
active: false,
...req.query,
};
const vouchers = await Voucher.query()
.onBuild((builder) => {
builder.modify('filter', filter);
builder.orderBy('createdAt', 'ASC');
});
return res.status(200).send({ vouchers });
}
}

View File

@@ -0,0 +1,22 @@
import { Router } from 'express'
import { Container, Service } from 'typedi';
import JWTAuth from '@/http/middleware/jwtAuth';
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
import PaymentViaVoucherController from '@/http/controllers/Subscription/PaymentViaVoucher';
@Service()
export default class SubscriptionController {
/**
* Router constructor.
*/
router() {
const router = Router();
router.use(JWTAuth);
router.use(TenancyMiddleware);
router.use('/voucher', Container.get(PaymentViaVoucherController).router());
return router;
}
}

View File

@@ -1,5 +1,5 @@
// import OAuth2 from '@/http/controllers/OAuth2';
import express from 'express';
import { Container } from 'typedi';
import Authentication from '@/http/controllers/Authentication';
import InviteUsers from '@/http/controllers/InviteUsers';
import Users from '@/http/controllers/Users';
@@ -24,15 +24,24 @@ import JWTAuth from '@/http/middleware/jwtAuth';
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
import Ping from '@/http/controllers/Ping';
import Agendash from '@/http/controllers/Agendash';
import Subscription from '@/http/controllers/Subscription';
import VouchersController from '@/http/controllers/Subscription/Vouchers';
import TenantDependencyInjection from '@/http/middleware/TenantDependencyInjection';
import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware';
export default (app) => {
app.use('/api/auth', Authentication.router());
app.use('/api/invite', InviteUsers.router());
app.use('/api/vouchers', Container.get(VouchersController).router());
app.use('/api/subscription', Container.get(Subscription).router());
app.use('/api/ping', Container.get(Ping).router());
const dashboard = express.Router();
dashboard.use(JWTAuth);
dashboard.use(TenancyMiddleware);
dashboard.use(SubscriptionMiddleware('main'));
dashboard.use('/api/currencies', Currencies.router());
dashboard.use('/api/users', Users.router());
@@ -41,7 +50,7 @@ export default (app) => {
dashboard.use('/api/accounting', Accounting.router());
dashboard.use('/api/views', Views.router());
dashboard.use('/api/items', Items.router());
dashboard.use('/api/item_categories', ItemCategories.router());
dashboard.use('/api/item_categories', Container.get(ItemCategories));
dashboard.use('/api/expenses', Expenses.router());
dashboard.use('/api/financial_statements', FinancialStatements.router());
dashboard.use('/api/options', Options.router());
@@ -52,8 +61,7 @@ export default (app) => {
dashboard.use('/api/resources', Resources.router());
dashboard.use('/api/exchange_rates', ExchangeRates.router());
dashboard.use('/api/media', Media.router());
dashboard.use('/api/ping', Ping.router());
app.use('/agendash', Agendash.router());
app.use('/', dashboard);
};

View File

@@ -0,0 +1,26 @@
export default (subscriptionSlug = 'main') => async (req, res, next) => {
const { tenant } = req;
if (!tenant) {
throw new Error('Should load `TenancyMiddlware` before this middleware.');
}
const subscription = await tenant
.$relatedQuery('subscriptions')
.modify('subscriptionBySlug', subscriptionSlug)
.first();
// Validate in case there is no any already subscription.
if (!subscription) {
return res.status(400).send({
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
});
}
// Validate in case the subscription is inactive.
else if (subscription.inactive()) {
return res.status(400).send({
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
});
}
next();
};

View File

@@ -1,8 +0,0 @@
const subscriptionObserver = (req, res, next) => {
};
export default subscriptionObserver;

View File

@@ -44,6 +44,7 @@ export default async (req, res, next) => {
req.knex = knex;
req.organizationId = organizationId;
req.tenant = tenant;
req.models = {
...Object.values(models).reduce((acc, model) => {
if (typeof model.resource.default !== 'undefined' &&

View File

@@ -0,0 +1,13 @@
import { Request, Response } from 'express';
import { Container } from 'typedi';
export default async (req: Request, res: Response, next: Function) => {
const { organizationId, knex } = req;
if (!organizationId || !knex) {
throw new Error('Should load `TenancyMiddleware` before this middleware.');
}
Container.of(`tenant-${organizationId}`).set('knex', knex);
next();
};

View File

@@ -1,35 +1,34 @@
import { camelCase, snakeCase } from 'lodash';
import { Request, Response } from 'express';
import { camelCase, snakeCase, mapKeys } from 'lodash';
/**
* create a middleware to change json format from snake case to camelcase in request
* then change back to snake case in response
*
*/
export default function createMiddleware() {
return function (req, res, next) {
/**
* camelize req.body
*/
if (req.body && typeof req.body === 'object') {
req.body = camelCase(req.body);
}
/**
* camelize req.query
*/
if (req.query && typeof req.query === 'object') {
req.query = camelCase(req.query);
}
/**
* wrap res.json()
*/
const sendJson = res.json;
res.json = (data) => {
return sendJson.call(res, snakeCase(data));
}
return next();
export default (req: Request, res: Response, next: Function) => {
/**
* camelize `req.body`
*/
if (req.body && typeof req.body === 'object') {
req.body = mapKeys(req.body, (value: any, key: string) => camelCase(key));
}
}
/**
* camelize `req.query`
*/
if (req.query && typeof req.query === 'object') {
req.query = mapKeys(req.query, (value: any, key: string) => camelCase(key));
}
/**
* wrap `res.json()`
*/
const sendJson = res.json;
res.json = (data: any) => {
const mapped = mapKeys(data, (value: any, key: string) => snakeCase(key));
return sendJson.call(res, mapped);
};
return next();
};