fix: mark payment license as used after usage.

fix: fix system models with createdAt and updatedAt fields.
fix: reset password token expiration time.
This commit is contained in:
Ahmed Bouhuolia
2020-09-07 17:13:37 +02:00
parent d3870974c0
commit ffb0499280
24 changed files with 191 additions and 94 deletions

View File

@@ -0,0 +1,8 @@
export default class NoPaymentModelWithPricedPlan {
constructor() {
}
}

View File

@@ -1,9 +1,8 @@
export default class NotAllowedChangeSubscriptionPlan extends Error{
export default class NotAllowedChangeSubscriptionPlan {
constructor(message: string) {
super(message);
this.name = "NotAllowedChangeSubscriptionPlan";
}
}

View File

@@ -0,0 +1,7 @@
export default class PaymentAmountInvalidWithPlan{
constructor() {
}
}

View File

@@ -0,0 +1,5 @@
export default class PaymentInputInvalid {
constructor() {}
}

View File

@@ -1,9 +1,15 @@
import NotAllowedChangeSubscriptionPlan from './NotAllowedChangeSubscriptionPlan';
import ServiceError from './ServiceError';
import ServiceErrors from './ServiceErrors';
import NoPaymentModelWithPricedPlan from './NoPaymentModelWithPricedPlan';
import PaymentInputInvalid from './PaymentInputInvalid';
import PaymentAmountInvalidWithPlan from './PaymentAmountInvalidWithPlan';
export {
NotAllowedChangeSubscriptionPlan,
NoPaymentModelWithPricedPlan,
PaymentAmountInvalidWithPlan,
ServiceError,
ServiceErrors,
PaymentInputInvalid
};

View File

@@ -204,7 +204,7 @@ export default class AuthenticationController extends BaseController{
})
} catch(error) {
if (error instanceof ServiceError) {
if (error.errorType === 'token_invalid') {
if (error.errorType === 'token_invalid' || error.errorType === 'token_expired') {
return res.boom.badRequest(null, {
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
});

View File

@@ -1,13 +1,16 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { check, param, query, ValidationSchema } from 'express-validator';
import { License, Plan } from '@/system/models';
import { check } from 'express-validator';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import PaymentMethodController from '@/http/controllers/Subscription/PaymentMethod';
import {
NotAllowedChangeSubscriptionPlan
NotAllowedChangeSubscriptionPlan,
NoPaymentModelWithPricedPlan,
PaymentAmountInvalidWithPlan,
PaymentInputInvalid,
} from '@/exceptions';
import { ILicensePaymentModel } from '@/interfaces';
@Service()
export default class PaymentViaLicenseController extends PaymentMethodController {
@@ -24,9 +27,7 @@ export default class PaymentViaLicenseController extends PaymentMethodController
'/payment',
this.paymentViaLicenseSchema,
validateMiddleware,
asyncMiddleware(this.validateLicenseCodeExistance.bind(this)),
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
asyncMiddleware(this.validateLicenseAndPlan.bind(this)),
asyncMiddleware(this.paymentViaLicense.bind(this)),
);
return router;
@@ -38,54 +39,10 @@ export default class PaymentViaLicenseController extends PaymentMethodController
get paymentViaLicenseSchema() {
return [
check('plan_slug').exists().trim().escape(),
check('license_code').exists().trim().escape(),
check('license_code').optional().trim().escape(),
];
}
/**
* Validate the given license code exists on the storage.
* @async
* @param {Request} req
* @param {Response} res
*/
async validateLicenseCodeExistance(req: Request, res: Response, next: Function) {
const { licenseCode } = this.matchedBodyData(req);
this.logger.info('[license_payment] trying to validate license code.', { licenseCode });
const foundLicense = await License.query()
.modify('filterActiveLicense')
.where('license_code', licenseCode)
.first();
if (!foundLicense) {
return res.status(400).send({
errors: [{ type: 'LICENSE.CODE.IS.INVALID', code: 120 }],
});
}
next();
}
/**
* Validate the license period and plan period.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateLicenseAndPlan(req: Request, res: Response, next: Function) {
const { planSlug, licenseCode } = this.matchedBodyData(req);
this.logger.info('[license_payment] trying to validate license with the plan.', { licenseCode });
const license = await License.query().findOne('license_code', licenseCode);
const plan = await Plan.query().findOne('slug', planSlug);
if (license.planId !== plan.id) {
return res.status(400).send({
errors: [{ type: 'LICENSE.NOT.FOR.GIVEN.PLAN' }],
});
}
next();
}
/**
* Handle the subscription payment via license code.
* @param {Request} req
@@ -97,8 +54,11 @@ export default class PaymentViaLicenseController extends PaymentMethodController
const { tenant } = req;
try {
const licenseModel: ILicensePaymentModel|null = licenseCode
? { licenseCode } : null;
await this.subscriptionService
.subscriptionViaLicense(tenant.id, planSlug, licenseCode);
.subscriptionViaLicense(tenant.id, planSlug, licenseModel);
return res.status(200).send({
type: 'success',
@@ -108,7 +68,13 @@ export default class PaymentViaLicenseController extends PaymentMethodController
} catch (exception) {
const errorReasons = [];
if (exception.name === 'NotAllowedChangeSubscriptionPlan') {
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,
@@ -117,6 +83,16 @@ export default class PaymentViaLicenseController extends PaymentMethodController
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

@@ -32,6 +32,11 @@ export default class AccountTransaction extends mixin(TenantModel, [CachableMode
*/
static get modifiers() {
return {
/**
* Filters accounts by the given ids.
* @param {Query} query
* @param {number[]} accountsIds
*/
filterAccounts(query, accountsIds) {
if (accountsIds.length > 0) {
query.whereIn('account_id', accountsIds);

View File

@@ -203,7 +203,7 @@ export default class AuthenticationService {
// Delete all stored tokens of reset password that associate to the give email.
this.logger.info('[send_reset_password] trying to delete all tokens by email.');
await PasswordReset.query().where('email', email).delete();
this.deletePasswordResetToken(email);
const token = uniqid();
@@ -230,28 +230,44 @@ export default class AuthenticationService {
this.logger.info('[reset_password] token invalid.');
throw new ServiceError('token_invalid');
}
// Different between tokne creation datetime and current time.
if (moment().diff(tokenModel.createdAt, 'seconds') > config.resetPasswordSeconds) {
this.logger.info('[reset_password] token expired.');
// Deletes the expired token by expired token email.
await this.deletePasswordResetToken(tokenModel.email);
throw new ServiceError('token_expired');
}
const user = await SystemUser.query().findOne('email', tokenModel.email)
if (!user) {
throw new ServiceError('user_not_found');
}
const hashedPassword = await hashPassword(password);
this.logger.info('[reset_password] saving a new hashed password.');
await SystemUser.query()
.where('email', tokenModel.email)
.update({
password: hashedPassword,
});
// Delete the reset password token.
await PasswordReset.query().where('email', user.email).delete();
.update({ password: hashedPassword });
// Deletes the used token.
await this.deletePasswordResetToken(tokenModel.email);
// Triggers `onResetPassword` event.
this.eventDispatcher.dispatch(events.auth.resetPassword, { user, token, password });
this.logger.info('[reset_password] reset password success.');
}
/**
* Deletes the password reset token by the given email.
* @param {string} email
* @returns {Promise}
*/
private async deletePasswordResetToken(email: string) {
this.logger.info('[reset_password] trying to delete all tokens by email.');
return PasswordReset.query().where('email', email).delete();
}
/**
* Generates JWT token for the given user.
* @param {ISystemUser} user

View File

@@ -5,7 +5,7 @@ import {
EventDispatcher,
EventDispatcherInterface,
} from '@/decorators/eventDispatcher';
import { ServiceError, ServiceErrors } from "@/exceptions";
import { ServiceError } from "@/exceptions";
import { SystemUser, Invite, Tenant } from "@/system/models";
import { Option } from '@/models';
import { hashPassword } from '@/utils';
@@ -49,8 +49,6 @@ export default class InviteUserService {
this.logger.info('[aceept_invite] trying to hash the user password.');
const hashedPassword = await hashPassword(inviteUserInput.password);
console.log(inviteToken);
this.logger.info('[accept_invite] trying to update user details.');
const updateUserOper = SystemUser.query()
.where('email', inviteToken.email)
@@ -60,7 +58,7 @@ export default class InviteUserService {
invite_accepted_at: moment().format('YYYY-MM-DD'),
password: hashedPassword,
});
this.logger.info('[accept_invite] trying to delete the given token.');
const deleteInviteTokenOper = Invite.query().where('token', inviteToken.token).delete();

View File

@@ -42,7 +42,6 @@ export default class LicenseService {
});
}
/**
*
* @param {number} loop
@@ -52,7 +51,7 @@ export default class LicenseService {
*/
async generateLicenses(
loop = 1,
licensePeriod: numner,
licensePeriod: number,
periodInterval: string = 'days',
planId: number,
) {
@@ -67,7 +66,7 @@ export default class LicenseService {
/**
* Disables the given license id on the storage.
* @param {number} licenseId
* @param {number} licenseId
* @return {Promise}
*/
async disableLicense(licenseId: number) {

View File

@@ -1,14 +1,47 @@
import { License } from "@/system/models";
import PaymentMethod from '@/services/Payment/PaymentMethod';
import { Plan } from '@/system/models';
import { IPaymentMethod, ILicensePaymentModel } from '@/interfaces';
import { ILicensePaymentModel } from "@/interfaces";
import { PaymentInputInvalid, PaymentAmountInvalidWithPlan } from '@/exceptions';
export default class VocuherPaymentMethod extends PaymentMethod implements IPaymentMethod {
export default class LicensePaymentMethod extends PaymentMethod implements IPaymentMethod {
/**
* Payment subscription of organization via license code.
* @param {ILicensePaymentModel}
* @param {ILicensePaymentModel} licensePaymentModel -
*/
async payment(licensePaymentModel: ILicensePaymentModel) {
async payment(licensePaymentModel: ILicensePaymentModel, plan: Plan) {
const license = await this.getLicenseOrThrowInvalid(licensePaymentModel);
this.validatePaymentAmountWithPlan(license, plan);
// Mark the license code as used.
return License.markLicenseAsUsed(licensePaymentModel.licenseCode);
}
/**
* Validates the license code activation on the storage.
* @param {ILicensePaymentModel} licensePaymentModel -
*/
async getLicenseOrThrowInvalid(licensePaymentModel: ILicensePaymentModel) {
const foundLicense = await License.query()
.modify('filterActiveLicense')
.where('license_code', licensePaymentModel.licenseCode)
.first();
if (!foundLicense) {
throw new PaymentInputInvalid();
}
return foundLicense;
}
/**
* Validates the payment amount with given plan price.
* @param {License} license
* @param {Plan} plan
*/
validatePaymentAmountWithPlan(license: License, plan: Plan) {
if (license.planId !== plan.id) {
throw new PaymentAmountInvalidWithPlan();
}
}
}

View File

@@ -1,4 +1,5 @@
import { IPaymentMethod, IPaymentContext } from "@/interfaces";
import { Plan } from '@/system/models';
export default class PaymentContext<PaymentModel> implements IPaymentContext{
paymentMethod: IPaymentMethod;
@@ -15,7 +16,7 @@ export default class PaymentContext<PaymentModel> implements IPaymentContext{
*
* @param {<PaymentModel>} paymentModel
*/
makePayment(paymentModel: PaymentModel) {
this.paymentMethod.makePayment(paymentModel);
makePayment(paymentModel: PaymentModel, plan: Plan) {
return this.paymentMethod.payment(paymentModel, plan);
}
}

View File

@@ -1,7 +1,8 @@
import { Inject } from 'typedi';
import { Inject, Service } from 'typedi';
import { Tenant, Plan } from '@/system/models';
import { IPaymentContext } from '@/interfaces';
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
import { NoPaymentModelWithPricedPlan } from '@/exceptions';
export default class Subscription<PaymentModel> {
paymentContext: IPaymentContext|null;
@@ -19,7 +20,7 @@ export default class Subscription<PaymentModel> {
/**
* Subscripe to the given plan.
* @param {Plan} plan
* @param {Plan} plan
* @throws {NotAllowedChangeSubscriptionPlan}
*/
async subscribe(
@@ -28,8 +29,11 @@ export default class Subscription<PaymentModel> {
paymentModel?: PaymentModel,
subscriptionSlug: string = 'main',
) {
if (plan.price < 0) {
await this.paymentContext.makePayment(paymentModel);
this.validateIfPlanHasPriceNoPayment(plan, paymentModel);
// @todo
if (plan.price > 0) {
await this.paymentContext.makePayment(paymentModel, plan);
}
const subscription = await tenant.$relatedQuery('subscriptions')
.modify('subscriptionBySlug', subscriptionSlug)
@@ -39,8 +43,7 @@ export default class Subscription<PaymentModel> {
if (subscription && subscription.active()) {
throw new NotAllowedChangeSubscriptionPlan;
// In case there is already subscription associated to the given tenant.
// renew it.
// In case there is already subscription associated to the given tenant renew it.
} else if(subscription && subscription.inactive()) {
await subscription.renew(plan);
@@ -49,4 +52,15 @@ export default class Subscription<PaymentModel> {
await tenant.newSubscription(subscriptionSlug, plan);
}
}
/**
* Throw error in plan has price and no payment model.
* @param {Plan} plan -
* @param {PaymentModel} paymentModel - payment input.
*/
validateIfPlanHasPriceNoPayment(plan: Plan, paymentModel: PaymentMode) {
if (plan.price > 0 && !paymentModel) {
throw new NoPaymentModelWithPricedPlan();
}
}
}

View File

@@ -1,10 +1,11 @@
import { Service, Inject } from 'typedi';
import { Plan, Tenant, License } from '@/system/models';
import { Plan, Tenant } from '@/system/models';
import Subscription from '@/services/Subscription/Subscription';
import VocuherPaymentMethod from '@/services/Payment/LicensePaymentMethod';
import LicensePaymentMethod from '@/services/Payment/LicensePaymentMethod';
import PaymentContext from '@/services/Payment';
import SubscriptionSMSMessages from '@/services/Subscription/SMSMessages';
import SubscriptionMailMessages from '@/services/Subscription/MailMessages';
import { ILicensePaymentModel } from '@/interfaces';
@Service()
export default class SubscriptionService {
@@ -14,31 +15,37 @@ export default class SubscriptionService {
@Inject()
mailMessages: SubscriptionMailMessages;
@Inject('logger')
logger: any;
/**
* Handles the payment process via license code and than subscribe to
* the given tenant.
*
* @param {number} tenantId
* @param {String} planSlug
* @param {string} licenseCode
*
* @return {Promise}
*/
async subscriptionViaLicense(
tenantId: number,
planSlug: string,
licenseCode: string,
paymentModel?: ILicensePaymentModel,
subscriptionSlug: string = 'main',
) {
this.logger.info('[subscription_via_license] try to subscribe via given license.', {
tenantId, paymentModel
});
const plan = await Plan.query().findOne('slug', planSlug);
const tenant = await Tenant.query().findById(tenantId);
const licenseModel = await License.query().findOne('license_code', licenseCode);
const paymentViaLicense = new VocuherPaymentMethod();
const paymentViaLicense = new LicensePaymentMethod();
const paymentContext = new PaymentContext(paymentViaLicense);
const subscription = new Subscription(paymentContext);
return subscription.subscribe(tenant, plan, licenseModel, subscriptionSlug);
await subscription.subscribe(tenant, plan, paymentModel, subscriptionSlug);
this.logger.info('[subscription_via_license] payment via license done successfully.', {
tenantId, paymentModel
});
}
}

View File

@@ -5,7 +5,7 @@ exports.up = function(knex) {
table.string('email');
table.string('token').unique();
table.integer('tenant_id').unsigned();
table.timestamps();
table.datetime('created_at');
});
};

View File

@@ -7,4 +7,11 @@ export default class UserInvite extends SystemModel {
static get tableName() {
return 'user_invites';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt'];
}
}

View File

@@ -7,4 +7,11 @@ export default class PasswordResets extends SystemModel {
static get tableName() {
return 'password_resets';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt'];
}
}

View File

@@ -13,7 +13,7 @@ export default class Plan extends mixin(SystemModel) {
/**
* Timestamps columns.
*/
static get timestamps() {
get timestamps() {
return ['createdAt', 'updatedAt'];
}

View File

@@ -14,7 +14,7 @@ export default class PlanSubscription extends mixin(SystemModel) {
/**
* Timestamps columns.
*/
static get timestamps() {
get timestamps() {
return ['createdAt', 'updatedAt'];
}

View File

@@ -14,7 +14,7 @@ export default class SystemUser extends mixin(SystemModel) {
/**
* Timestamps columns.
*/
static get timestamps() {
get timestamps() {
return ['createdAt', 'updatedAt'];
}

View File

@@ -10,6 +10,13 @@ export default class Tenant extends BaseModel {
return 'tenants';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Query modifiers.
*/