mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
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:
6
server/src/services/Payment/PaymentMethod.ts
Normal file
6
server/src/services/Payment/PaymentMethod.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import { IPaymentModel } from '@/interfaces';
|
||||
|
||||
export default class PaymentMethod implements IPaymentModel {
|
||||
|
||||
}
|
||||
78
server/src/services/Payment/Voucher.ts
Normal file
78
server/src/services/Payment/Voucher.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
36
server/src/services/Payment/VoucherMailMessages.ts
Normal file
36
server/src/services/Payment/VoucherMailMessages.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
14
server/src/services/Payment/VoucherPaymentMethod.ts
Normal file
14
server/src/services/Payment/VoucherPaymentMethod.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
17
server/src/services/Payment/VoucherSMSMessages.ts
Normal file
17
server/src/services/Payment/VoucherSMSMessages.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
21
server/src/services/Payment/index.ts
Normal file
21
server/src/services/Payment/index.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
27
server/src/services/SMSClient/EasySmsClient.ts
Normal file
27
server/src/services/SMSClient/EasySmsClient.ts
Normal 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) });
|
||||
});
|
||||
}
|
||||
}
|
||||
13
server/src/services/SMSClient/SMSAPI.ts
Normal file
13
server/src/services/SMSClient/SMSAPI.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
5
server/src/services/SMSClient/SMSClientInterface.ts
Normal file
5
server/src/services/SMSClient/SMSClientInterface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
export default interface SMSClientInterface {
|
||||
clientName: string;
|
||||
send(to: string, message: string): boolean;
|
||||
}
|
||||
3
server/src/services/SMSClient/index.ts
Normal file
3
server/src/services/SMSClient/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SMSAPI from './SMSAPI';
|
||||
|
||||
export default SMSAPI;
|
||||
@@ -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,
|
||||
|
||||
48
server/src/services/Subscription/Subscription.ts
Normal file
48
server/src/services/Subscription/Subscription.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
server/src/services/Subscription/SubscriptionPeriod.ts
Normal file
41
server/src/services/Subscription/SubscriptionPeriod.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
36
server/src/services/Subscription/SubscriptionService.ts
Normal file
36
server/src/services/Subscription/SubscriptionService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
|
||||
export default (Model) => {
|
||||
return class UserSubscription extends Model{
|
||||
|
||||
onTrial() {
|
||||
|
||||
}
|
||||
|
||||
getSubscription() {
|
||||
|
||||
}
|
||||
|
||||
newSubscription() {
|
||||
|
||||
}
|
||||
|
||||
isSubcribedTo(plan) {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user