feat(server): deprecated the subscription module.

This commit is contained in:
a.bouhuolia
2023-03-02 22:44:14 +02:00
parent 44fc26b156
commit 3b79ac66ae
37 changed files with 8 additions and 1501 deletions

View File

@@ -150,7 +150,6 @@ export default class OrganizationService {
public async currentOrganization(tenantId: number): Promise<ITenant> {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('subscriptions')
.withGraphFetched('metadata');
this.throwIfTenantNotExists(tenant);

View File

@@ -1,185 +0,0 @@
import { Service, Container, Inject } from 'typedi';
import cryptoRandomString from 'crypto-random-string';
import { times } from 'lodash';
import { License, Plan } from '@/system/models';
import { ILicense, ISendLicenseDTO } from '@/interfaces';
import LicenseMailMessages from '@/services/Payment/LicenseMailMessages';
import LicenseSMSMessages from '@/services/Payment/LicenseSMSMessages';
import { ServiceError } from '@/exceptions';
const ERRORS = {
PLAN_NOT_FOUND: 'PLAN_NOT_FOUND',
LICENSE_NOT_FOUND: 'LICENSE_NOT_FOUND',
LICENSE_ALREADY_DISABLED: 'LICENSE_ALREADY_DISABLED',
NO_AVALIABLE_LICENSE_CODE: 'NO_AVALIABLE_LICENSE_CODE',
};
@Service()
export default class LicenseService {
@Inject()
smsMessages: LicenseSMSMessages;
@Inject()
mailMessages: LicenseMailMessages;
/**
* Validate the plan existance on the storage.
* @param {number} tenantId -
* @param {string} planSlug - Plan slug.
*/
private async getPlanOrThrowError(planSlug: string) {
const foundPlan = await Plan.query().where('slug', planSlug).first();
if (!foundPlan) {
throw new ServiceError(ERRORS.PLAN_NOT_FOUND);
}
return foundPlan;
}
/**
* Valdiate the license existance on the storage.
* @param {number} licenseId - License id.
*/
private async getLicenseOrThrowError(licenseId: number) {
const foundLicense = await License.query().findById(licenseId);
if (!foundLicense) {
throw new ServiceError(ERRORS.LICENSE_NOT_FOUND);
}
return foundLicense;
}
/**
* Validates whether the license id is disabled.
* @param {ILicense} license
*/
private validateNotDisabledLicense(license: ILicense) {
if (license.disabledAt) {
throw new ServiceError(ERRORS.LICENSE_ALREADY_DISABLED);
}
}
/**
* Generates the license code in the given period.
* @param {number} licensePeriod
* @return {Promise<ILicense>}
*/
public async generateLicense(
licensePeriod: number,
periodInterval: string = 'days',
planSlug: string
): ILicense {
let licenseCode: string;
let repeat: boolean = true;
// Retrieve plan or throw not found error.
const plan = await this.getPlanOrThrowError(planSlug);
while (repeat) {
licenseCode = cryptoRandomString({ length: 10, type: 'numeric' });
const foundLicenses = await License.query().where(
'license_code',
licenseCode
);
if (foundLicenses.length === 0) {
repeat = false;
}
}
return License.query().insert({
licenseCode,
licensePeriod,
periodInterval,
planId: plan.id,
});
}
/**
* Generates licenses.
* @param {number} loop
* @param {number} licensePeriod
* @param {string} periodInterval
* @param {number} planId
*/
public async generateLicenses(
loop = 1,
licensePeriod: number,
periodInterval: string = 'days',
planSlug: string
) {
const asyncOpers: Promise<any>[] = [];
times(loop, () => {
const generateOper = this.generateLicense(
licensePeriod,
periodInterval,
planSlug
);
asyncOpers.push(generateOper);
});
return Promise.all(asyncOpers);
}
/**
* Disables the given license id on the storage.
* @param {string} licenseSlug - License slug.
* @return {Promise}
*/
public async disableLicense(licenseId: number) {
const license = await this.getLicenseOrThrowError(licenseId);
this.validateNotDisabledLicense(license);
return License.markLicenseAsDisabled(license.id, 'id');
}
/**
* Deletes the given license id from the storage.
* @param licenseSlug {string} - License slug.
*/
public async deleteLicense(licenseSlug: string) {
const license = await this.getPlanOrThrowError(licenseSlug);
return License.query().where('id', license.id).delete();
}
/**
* Sends license code to the given customer via SMS or mail message.
* @param {string} licenseCode - License code.
* @param {string} phoneNumber - Phone number.
* @param {string} email - Email address.
*/
public async sendLicenseToCustomer(sendLicense: ISendLicenseDTO) {
const agenda = Container.get('agenda');
const { phoneNumber, email, period, periodInterval } = sendLicense;
// Retreive plan details byt the given plan slug.
const plan = await this.getPlanOrThrowError(sendLicense.planSlug);
const license = await License.query()
.modify('filterActiveLicense')
.where('license_period', period)
.where('period_interval', periodInterval)
.where('plan_id', plan.id)
.first();
if (!license) {
throw new ServiceError(ERRORS.NO_AVALIABLE_LICENSE_CODE)
}
// Mark the license as used.
await License.markLicenseAsSent(license.licenseCode);
if (sendLicense.email) {
await agenda.schedule('1 second', 'send-license-via-email', {
licenseCode: license.licenseCode,
email,
});
}
if (phoneNumber) {
await agenda.schedule('1 second', 'send-license-via-phone', {
licenseCode: license.licenseCode,
phoneNumber,
});
}
}
}

View File

@@ -1,26 +0,0 @@
import { Container } from 'typedi';
import Mail from '@/lib/Mail';
import config from '@/config';
export default class SubscriptionMailMessages {
/**
* Send license code to the given mail address.
* @param {string} licenseCode
* @param {email} email
*/
public async sendMailLicense(licenseCode: string, email: string) {
const Logger = Container.get('logger');
const mail = new Mail()
.setView('mail/LicenseReceive.html')
.setSubject('Bigcapital - License code')
.setTo(email)
.setData({
licenseCode,
successEmail: config.customerSuccess.email,
successPhoneNumber: config.customerSuccess.phoneNumber,
});
await mail.send();
Logger.info('[license_mail] sent successfully.');
}
}

View File

@@ -1,67 +0,0 @@
import { License } from '@/system/models';
import PaymentMethod from '@/services/Payment/PaymentMethod';
import { Plan } from '@/system/models';
import { IPaymentMethod, ILicensePaymentModel } from '@/interfaces';
import {
PaymentInputInvalid,
PaymentAmountInvalidWithPlan,
VoucherCodeRequired,
} from '@/exceptions';
export default class LicensePaymentMethod
extends PaymentMethod
implements IPaymentMethod
{
/**
* Payment subscription of organization via license code.
* @param {ILicensePaymentModel} licensePaymentModel -
*/
public async payment(licensePaymentModel: ILicensePaymentModel, plan: Plan) {
this.validateLicensePaymentModel(licensePaymentModel);
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 -
*/
private 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
*/
private validatePaymentAmountWithPlan(license: License, plan: Plan) {
if (license.planId !== plan.id) {
throw new PaymentAmountInvalidWithPlan();
}
}
/**
* Validate voucher payload.
* @param {ILicensePaymentModel} licenseModel -
*/
private validateLicensePaymentModel(licenseModel: ILicensePaymentModel) {
if (!licenseModel || !licenseModel.licenseCode) {
throw new VoucherCodeRequired();
}
}
}

View File

@@ -1,17 +0,0 @@
import { Container, Inject } from 'typedi';
import SMSClient from '@/services/SMSClient';
export default class SubscriptionSMSMessages {
@Inject('SMSClient')
smsClient: SMSClient;
/**
* Sends license code to the given phone number via SMS message.
* @param {string} phoneNumber
* @param {string} licenseCode
*/
public async sendLicenseSMSMessage(phoneNumber: string, licenseCode: string) {
const message: string = `Your license card number: ${licenseCode}. If you need any help please contact us. Bigcapital.`;
return this.smsClient.sendMessage(phoneNumber, message);
}
}

View File

@@ -1,6 +0,0 @@
import moment from 'moment';
import { IPaymentModel } from '@/interfaces';
export default class PaymentMethod implements IPaymentModel {
}

View File

@@ -1,22 +0,0 @@
import { IPaymentMethod, IPaymentContext } from "interfaces";
import { Plan } from '@/system/models';
export default class PaymentContext<PaymentModel> implements IPaymentContext{
paymentMethod: IPaymentMethod;
/**
* Constructor method.
* @param {IPaymentMethod} paymentMethod
*/
constructor(paymentMethod: IPaymentMethod) {
this.paymentMethod = paymentMethod;
}
/**
*
* @param {<PaymentModel>} paymentModel
*/
makePayment(paymentModel: PaymentModel, plan: Plan) {
return this.paymentMethod.payment(paymentModel, plan);
}
}

View File

@@ -1,30 +0,0 @@
import { Service } from "typedi";
@Service()
export default class SubscriptionMailMessages {
/**
*
* @param phoneNumber
* @param remainingDays
*/
public async sendRemainingSubscriptionPeriod(phoneNumber: string, remainingDays: number) {
const message: string = `
Your remaining subscription is ${remainingDays} days,
please renew your subscription before expire.
`;
this.smsClient.sendMessage(phoneNumber, message);
}
/**
*
* @param phoneNumber
* @param remainingDays
*/
public async sendRemainingTrialPeriod(phoneNumber: string, remainingDays: number) {
const message: string = `
Your remaining free trial is ${remainingDays} days,
please subscription before ends, if you have any quation to contact us.`;
this.smsClient.sendMessage(phoneNumber, message);
}
}

View File

@@ -1,40 +0,0 @@
import { Service, Inject } from 'typedi';
import SMSClient from '@/services/SMSClient';
@Service()
export default class SubscriptionSMSMessages {
@Inject('SMSClient')
smsClient: SMSClient;
/**
* Send remaining subscription period SMS message.
* @param {string} phoneNumber -
* @param {number} remainingDays -
*/
public async sendRemainingSubscriptionPeriod(
phoneNumber: string,
remainingDays: number
): Promise<void> {
const message: string = `
Your remaining subscription is ${remainingDays} days,
please renew your subscription before expire.
`;
this.smsClient.sendMessage(phoneNumber, message);
}
/**
* Send remaining trial period SMS message.
* @param {string} phoneNumber -
* @param {number} remainingDays -
*/
public async sendRemainingTrialPeriod(
phoneNumber: string,
remainingDays: number
): Promise<void> {
const message: string = `
Your remaining free trial is ${remainingDays} days,
please subscription before ends, if you have any quation to contact us.`;
this.smsClient.sendMessage(phoneNumber, message);
}
}

View File

@@ -1,80 +0,0 @@
import { Inject } from 'typedi';
import { Tenant, Plan } from '@/system/models';
import { IPaymentContext } from '@/interfaces';
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
export default class Subscription<PaymentModel> {
paymentContext: IPaymentContext | null;
@Inject('logger')
logger: any;
/**
* Constructor method.
* @param {IPaymentContext}
*/
constructor(payment?: IPaymentContext) {
this.paymentContext = payment;
}
/**
* Give the tenant a new subscription.
* @param {Tenant} tenant
* @param {Plan} plan
* @param {string} invoiceInterval
* @param {number} invoicePeriod
* @param {string} subscriptionSlug
*/
protected async newSubscribtion(
tenant,
plan,
invoiceInterval: string,
invoicePeriod: number,
subscriptionSlug: string = 'main'
) {
const subscription = await tenant
.$relatedQuery('subscriptions')
.modify('subscriptionBySlug', subscriptionSlug)
.first();
// No allowed to re-new the the subscription while the subscription is active.
if (subscription && subscription.active()) {
throw new NotAllowedChangeSubscriptionPlan();
// In case there is already subscription associated to the given tenant renew it.
} else if (subscription && subscription.inactive()) {
await subscription.renew(invoiceInterval, invoicePeriod);
// No stored past tenant subscriptions create new one.
} else {
await tenant.newSubscription(
plan.id,
invoiceInterval,
invoicePeriod,
subscriptionSlug
);
}
}
/**
* Subscripe to the given plan.
* @param {Plan} plan
* @throws {NotAllowedChangeSubscriptionPlan}
*/
public async subscribe(
tenant: Tenant,
plan: Plan,
paymentModel?: PaymentModel,
subscriptionSlug: string = 'main'
) {
await this.paymentContext.makePayment(paymentModel, plan);
return this.newSubscribtion(
tenant,
plan,
plan.invoiceInterval,
plan.invoicePeriod,
subscriptionSlug
);
}
}

View File

@@ -1,41 +0,0 @@
import moment from 'moment';
export default class SubscriptionPeriod {
start: Date;
end: Date;
interval: string;
count: number;
/**
* Constructor method.
* @param {string} interval -
* @param {number} count -
* @param {Date} start -
*/
constructor(interval: string = 'month', count: number, start?: Date) {
this.interval = interval;
this.count = count;
this.start = start;
if (!start) {
this.start = moment().toDate();
}
this.end = moment(start).add(count, interval).toDate();
}
getStartDate() {
return this.start;
}
getEndDate() {
return this.end;
}
getInterval() {
return this.interval;
}
getIntervalCount() {
return this.interval;
}
}

View File

@@ -1,69 +0,0 @@
import { Service, Inject } from 'typedi';
import { Plan, PlanSubscription, Tenant } from '@/system/models';
import Subscription from '@/services/Subscription/Subscription';
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';
import SubscriptionViaLicense from './SubscriptionViaLicense';
@Service()
export default class SubscriptionService {
@Inject()
smsMessages: SubscriptionSMSMessages;
@Inject()
mailMessages: SubscriptionMailMessages;
@Inject('logger')
logger: any;
@Inject('repositories')
sysRepositories: 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}
*/
public async subscriptionViaLicense(
tenantId: number,
planSlug: string,
paymentModel: ILicensePaymentModel,
subscriptionSlug: string = 'main'
) {
// Retrieve plan details.
const plan = await Plan.query().findOne('slug', planSlug);
// Retrieve tenant details.
const tenant = await Tenant.query().findById(tenantId);
// License payment method.
const paymentViaLicense = new LicensePaymentMethod();
// Payment context.
const paymentContext = new PaymentContext(paymentViaLicense);
// Subscription.
const subscription = new SubscriptionViaLicense(paymentContext);
// Subscribe.
await subscription.subscribe(tenant, plan, paymentModel, subscriptionSlug);
}
/**
* Retrieve all subscription of the given tenant.
* @param {number} tenantId
*/
public async getSubscriptions(tenantId: number) {
const subscriptions = await PlanSubscription.query().where(
'tenant_id',
tenantId
);
return subscriptions;
}
}

View File

@@ -1,54 +0,0 @@
import { License, Tenant, Plan } from '@/system/models';
import Subscription from './Subscription';
import { PaymentModel } from '@/interfaces';
export default class SubscriptionViaLicense extends Subscription<PaymentModel> {
/**
* Subscripe to the given plan.
* @param {Plan} plan
* @throws {NotAllowedChangeSubscriptionPlan}
*/
public async subscribe(
tenant: Tenant,
plan: Plan,
paymentModel?: PaymentModel,
subscriptionSlug: string = 'main'
): Promise<void> {
await this.paymentContext.makePayment(paymentModel, plan);
return this.newSubscriptionFromLicense(
tenant,
plan,
paymentModel.licenseCode,
subscriptionSlug
);
}
/**
* New subscription from the given license.
* @param {Tanant} tenant
* @param {Plab} plan
* @param {string} licenseCode
* @param {string} subscriptionSlug
* @returns {Promise<void>}
*/
private async newSubscriptionFromLicense(
tenant,
plan,
licenseCode: string,
subscriptionSlug: string = 'main'
): Promise<void> {
// License information.
const licenseInfo = await License.query().findOne(
'licenseCode',
licenseCode
);
return this.newSubscribtion(
tenant,
plan,
licenseInfo.periodInterval,
licenseInfo.licensePeriod,
subscriptionSlug
);
}
}