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

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

View File

@@ -0,0 +1,78 @@
import { Service, Container, Inject } from 'typedi';
import cryptoRandomString from 'crypto-random-string';
import { Voucher } from "@/system/models";
import { IVoucher } from '@/interfaces';
import VoucherMailMessages from '@/services/Payment/VoucherMailMessages';
import VoucherSMSMessages from '@/services/Payment/VoucherSMSMessages';
@Service()
export default class VoucherService {
@Inject()
smsMessages: VoucherSMSMessages;
@Inject()
mailMessages: VoucherMailMessages;
/**
* Generates the voucher code in the given period.
* @param {number} voucherPeriod
* @return {Promise<IVoucher>}
*/
async generateVoucher(
voucherPeriod: number,
periodInterval: string = 'days',
planId: number,
): IVoucher {
let voucherCode: string;
let repeat: boolean = true;
while(repeat) {
voucherCode = cryptoRandomString({ length: 10, type: 'numeric' });
const foundVouchers = await Voucher.query().where('voucher_code', voucherCode);
if (foundVouchers.length === 0) {
repeat = false;
}
}
return Voucher.query().insert({
voucherCode, voucherPeriod, periodInterval, planId,
});
}
/**
* Disables the given voucher id on the storage.
* @param {number} voucherId
* @return {Promise}
*/
async disableVoucher(voucherId: number) {
return Voucher.markVoucherAsDisabled(voucherId, 'id');
}
/**
* Deletes the given voucher id from the storage.
* @param voucherId
*/
async deleteVoucher(voucherId: number) {
return Voucher.query().where('id', voucherId).delete();
}
/**
* Sends voucher code to the given customer via SMS or mail message.
* @param {string} voucherCode - Voucher code
* @param {string} phoneNumber - Phone number
* @param {string} email - Email address.
*/
async sendVoucherToCustomer(voucherCode: string, phoneNumber: string, email: string) {
const agenda = Container.get('agenda');
// Mark the voucher as used.
await Voucher.markVoucherAsSent(voucherCode);
if (email) {
await agenda.schedule('1 second', 'send-voucher-via-email', { voucherCode, email });
}
if (phoneNumber) {
await agenda.schedule('1 second', 'send-voucher-via-phone', { voucherCode, phoneNumber });
}
}
}

View File

@@ -0,0 +1,36 @@
import fs from 'fs';
import path from 'path';
import Mustache from 'mustache';
import { Container } from 'typedi';
import mail from '@/services/mail';
export default class SubscriptionMailMessages {
/**
* Send voucher code to the given mail address.
* @param {string} voucherCode
* @param {email} email
*/
public async sendMailVoucher(voucherCode: string, email: string) {
const logger = Container.get('logger');
const filePath = path.join(global.rootPath, 'views/mail/VoucherReceive.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, { voucherCode });
const mailOptions = {
to: email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: 'Bigcapital Voucher',
html: rendered,
};
return new Promise((resolve, reject) => {
mail.sendMail(mailOptions, (error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
}

View File

@@ -0,0 +1,14 @@
import { Voucher } from "@/system/models";
import PaymentMethod from '@/services/Payment/PaymentMethod';
import { IPaymentMethod, IVoucherPaymentModel } from '@/interfaces';
export default class VocuherPaymentMethod extends PaymentMethod implements IPaymentMethod {
/**
* Payment subscription of organization via voucher code.
* @param {IVoucherPaymentModel}
*/
async payment(voucherPaymentModel: IVoucherPaymentModel) {
// Mark the voucher code as used.
return Voucher.markVoucherAsUsed(voucherPaymentModel.voucherCode);
}
}

View File

@@ -0,0 +1,17 @@
import { Container, Inject } from 'typedi';
import SMSClient from '@/services/SMSClient';
export default class SubscriptionSMSMessages {
@Inject('SMSClient')
smsClient: SMSClient;
/**
* Sends voucher code to the given phone number via SMS message.
* @param {string} phoneNumber
* @param {string} voucherCode
*/
public async sendVoucherSMSMessage(phoneNumber: string, voucherCode: string) {
const message: string = `Your voucher card number: ${voucherCode}.`;
return this.smsClient.sendMessage(phoneNumber, message);
}
}

View File

@@ -0,0 +1,21 @@
import { IPaymentMethod, IPaymentContext } from "@/interfaces";
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) {
this.paymentMethod.makePayment(paymentModel);
}
}

View File

@@ -0,0 +1,27 @@
import axios from 'axios';
import SMSClientInterface from '@/services/SMSClient/SMSClientInterfaces';
import config from '@/../config/config';
export default class EasySMSClient implements SMSClientInterface {
clientName: string = 'easysms';
/**
* Send message to given phone number via easy SMS client.
* @param {string} to
* @param {string} message
*/
send(to: string, message: string) {
console.log(config);
const API_KEY = config.easySMSGateway.api_key;
const params = `action=send-sms&api_key=${API_KEY}=&to=${to}&sms=${message}&unicode=1`;
return new Promise((resolve, reject) => {
axios.get(`https://easysms.devs.ly/sms/api?${params}`)
.then((response) => {
if (response.code === 'ok') { resolve(); }
else { reject(); }
})
.catch((error) => { reject(error) });
});
}
}

View File

@@ -0,0 +1,13 @@
import SMSClientInterface from '@/services/SMSClient/SMSClientInterface';
export default class SMSAPI {
smsClient: SMSClientInterface;
constructor(smsClient: SMSClientInterface){
this.smsClient = smsClient;
}
sendMessage(to: string, message: string, extraParams: [], extraHeaders: []) {
return this.smsClient.send(to, message);
}
}

View File

@@ -0,0 +1,5 @@
export default interface SMSClientInterface {
clientName: string;
send(to: string, message: string): boolean;
}

View File

@@ -0,0 +1,3 @@
import SMSAPI from './SMSAPI';
export default SMSAPI;

View File

@@ -30,7 +30,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const balance = sumBy(saleInvoiceDTO.entries, 'amount');
const invLotNumber = await InventoryService.nextLotNumber();
const saleInvoice = {
...formatDateFields(saleInvoiceDTO, ['invoide_date', 'due_date']),
...formatDateFields(saleInvoiceDTO, ['invoice_date', 'due_date']),
balance,
paymentAmount: 0,
invLotNumber,

View File

@@ -0,0 +1,48 @@
import { Tenant, Plan } from '@/system/models';
import { IPaymentContext } from '@/interfaces';
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
export default class Subscription<PaymentModel> {
paymentContext: IPaymentContext|null;
/**
* Constructor method.
* @param {IPaymentContext}
*/
constructor(payment?: IPaymentContext) {
this.paymentContext = payment;
}
/**
* Subscripe to the given plan.
* @param {Plan} plan
* @throws {NotAllowedChangeSubscriptionPlan}
*/
async subscribe(
tenant: Tenant,
plan: Plan,
paymentModel?: PaymentModel,
subscriptionSlug: string = 'main',
) {
if (plan.price < 0) {
await this.paymentContext.makePayment(paymentModel);
}
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(plan);
// No stored past tenant subscriptions create new one.
} else {
await tenant.newSubscription(subscriptionSlug, plan);
}
}
}

View File

@@ -0,0 +1,41 @@
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

@@ -0,0 +1,36 @@
import { Service } from 'typedi';
import { Plan, Tenant, Voucher } from '@/system/models';
import Subscription from '@/services/Subscription/Subscription';
import VocuherPaymentMethod from '@/services/Payment/VoucherPaymentMethod';
import PaymentContext from '@/services/Payment';
@Service()
export default class SubscriptionService {
/**
* Handles the payment process via voucher code and than subscribe to
* the given tenant.
*
* @param {number} tenantId
* @param {String} planSlug
* @param {string} voucherCode
*
* @return {Promise}
*/
async subscriptionViaVoucher(
tenantId: number,
planSlug: string,
voucherCode: string,
subscriptionSlug: string = 'main',
) {
const plan = await Plan.query().findOne('slug', planSlug);
const tenant = await Tenant.query().findById(tenantId);
const voucherModel = await Voucher.query().findOne('voucher_code', voucherCode);
const paymentViaVoucher = new VocuherPaymentMethod();
const paymentContext = new PaymentContext(paymentViaVoucher);
const subscription = new Subscription(paymentContext);
return subscription.subscribe(tenant, plan, voucherModel, subscriptionSlug);
}
}

View File

@@ -1,22 +0,0 @@
export default (Model) => {
return class UserSubscription extends Model{
onTrial() {
}
getSubscription() {
}
newSubscription() {
}
isSubcribedTo(plan) {
}
}
};