add invoice service

This commit is contained in:
Simon Larsen
2023-06-08 14:38:55 +01:00
parent 673f97404b
commit aed03ffcb3
7 changed files with 97 additions and 21 deletions

View File

@@ -1,7 +1,7 @@
enum SmsStatus {
Success = 'Success',
Error = 'Error',
LowBalance = 'Low Balance',
LowBalance = 'Low Balance',
}
export default SmsStatus;

View File

@@ -497,6 +497,35 @@ export class BillingService {
});
}
public static async genrateInvoiceAndChargeCustomer(
customerId: string,
itemText: string,
amountInUsd: number
): Promise<void> {
const invoice: Stripe.Invoice = await this.stripe.invoices.create({
customer: customerId,
auto_advance: true, // do not automatically charge.
collection_method: 'charge_automatically',
});
if (!invoice || !invoice.id) {
throw new APIException('Invoice not generated.');
}
await this.stripe.invoiceItems.create({
invoice: invoice.id,
amount: amountInUsd * 100,
quantity: 1,
description: itemText,
currency: 'usd',
customer: customerId,
});
await this.stripe.invoices.finalizeInvoice(invoice.id!);
await this.stripe.invoices.pay(invoice.id);
}
public static async payInvoice(
customerId: string,
invoiceId: string

View File

@@ -12,12 +12,13 @@ import ObjectID from 'Common/Types/ObjectID';
export default class SmsService {
public static async sendSms(
to: Phone, message: string, options: {
projectId?: ObjectID | undefined // project id for sms log
from?: Phone, // from phone number
to: Phone,
message: string,
options: {
projectId?: ObjectID | undefined; // project id for sms log
from?: Phone; // from phone number
}
): Promise<HTTPResponse<EmptyResponseData>> {
const body: JSONObject = {
to: to.toString(),
message,

View File

@@ -35,8 +35,6 @@ const Detail: Function = (props: ComponentProps): ReactElement => {
return <MarkdownViewer text={text} />;
};
const getDropdownViewer: Function = (
data: string,
options: Array<DropdownOption>,
@@ -76,7 +74,7 @@ const Detail: Function = (props: ComponentProps): ReactElement => {
};
const getUSDCentsField: Function = (usdCents: number): ReactElement => {
return <div>{usdCents/100} USD</div>;
return <div>{usdCents / 100} USD</div>;
};
const getField: Function = (field: Field, index: number): ReactElement => {
@@ -288,5 +286,3 @@ const Detail: Function = (props: ComponentProps): ReactElement => {
};
export default Detail;

View File

@@ -18,10 +18,14 @@ router.post(
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body);
await SmsService.sendSms(body['to'] as Phone, body['message'] as string, {
projectId: body['projectId'] as ObjectID,
from: body['from'] as Phone,
});
await SmsService.sendSms(
body['to'] as Phone,
body['message'] as string,
{
projectId: body['projectId'] as ObjectID,
from: body['from'] as Phone,
}
);
return Response.sendEmptyResponse(req, res);
}

View File

@@ -28,7 +28,12 @@ export const InternalSmtpFromName: string =
export const TwilioAccountSid: string = process.env['TWILIO_ACCOUNT_SID'] || '';
export const TwilioAuthToken: string = process.env['TWILIO_AUTH_TOKEN'] || '';
export const TwilioPhoneNumber: string = process.env['TWILIO_PHONE_NUMBER'] || '';
export const SMSDefaultCostInCents: number = process.env['SMS_DEFAULT_COST_IN_CENTS'] ? parseInt(process.env['SMS_DEFAULT_COST_IN_CENTS']) : 0;
export const TwilioPhoneNumber: string =
process.env['TWILIO_PHONE_NUMBER'] || '';
export const SMSDefaultCostInCents: number = process.env[
'SMS_DEFAULT_COST_IN_CENTS'
]
? parseInt(process.env['SMS_DEFAULT_COST_IN_CENTS'])
: 0;
export const SendGridApiKey: string = process.env['SENDGRID_API_KEY'] || '';

View File

@@ -10,6 +10,8 @@ import SmsLogService from "CommonServer/Services/SmsLogService"
import ProjectService from "CommonServer/Services/ProjectService";
import Project from "Model/Models/Project";
import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
import BillingService from "CommonServer/Services/BillingService";
import logger from "CommonServer/Utils/Logger";
export default class SmsService {
public static async sendSms(to: Phone, message: string, options: {
@@ -48,12 +50,16 @@ export default class SmsService {
// make sure project has enough balance.
if (options.projectId && IsBillingEnabled) {
project = await ProjectService.findOneById({
id: options.projectId,
select: {
smsOrCallCurrentBalanceInUSDCents: true,
enableAutoRechargeSmsOrCallBalance: true,
enableSmsNotifications: true
enableSmsNotifications: true,
autoRechargeSmsOrCallByBalanceInUSD: true,
autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true,
paymentProviderCustomerId: true
},
props: {
isRoot: true
@@ -63,7 +69,7 @@ export default class SmsService {
if (!project) {
smsLog.status = SmsStatus.Error;
smsLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
await SmsLogService.create({
await SmsLogService.create({
data: smsLog,
props: {
isRoot: true
@@ -73,7 +79,7 @@ export default class SmsService {
}
if(!project.enableSmsNotifications){
if (!project.enableSmsNotifications) {
smsLog.status = SmsStatus.Error;
smsLog.statusMessage = `SMS notifications are not enabled for this project. Please enable SMS notifications in project settings.`;
await SmsLogService.create({
@@ -85,6 +91,39 @@ export default class SmsService {
return;
}
// check if auto recharge is enabled and current balance is low.
if (IsBillingEnabled && project && project.enableAutoRechargeSmsOrCallBalance && project.autoRechargeSmsOrCallByBalanceInUSD && project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD) {
if (project.smsOrCallCurrentBalanceInUSDCents && project.smsOrCallCurrentBalanceInUSDCents < project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD) {
try {
// recharge balance
const updatedAmount: number = Math.floor(project.smsOrCallCurrentBalanceInUSDCents + (project.autoRechargeSmsOrCallByBalanceInUSD * 100));
// If the recharge is succcessful, then update the project balance.
await BillingService.genrateInvoiceAndChargeCustomer(project.paymentProviderCustomerId!, "SMS or Call Balance Recharge", project.autoRechargeSmsOrCallByBalanceInUSD);
await ProjectService.updateOneById({
data: {
smsOrCallCurrentBalanceInUSDCents: updatedAmount
},
id: project.id!,
props: {
isRoot: true
}
});
project.smsOrCallCurrentBalanceInUSDCents = updatedAmount;
// TODO: Send an email on successful recharge.
} catch (err) {
// TODO: if the recharge fails, then send email to the user.
logger.error(err);
}
}
}
if (!project.smsOrCallCurrentBalanceInUSDCents) {
smsLog.status = SmsStatus.LowBalance;
smsLog.statusMessage = `Project ${options.projectId.toString()} does not have enough SMS balance.`;
@@ -96,7 +135,7 @@ export default class SmsService {
});
return;
}
if (project.smsOrCallCurrentBalanceInUSDCents < SMSDefaultCostInCents) {
@@ -109,7 +148,6 @@ export default class SmsService {
}
});
return;
}
}
@@ -153,6 +191,9 @@ export default class SmsService {
isRoot: true
}
});
}
}
}