mirror of
https://github.com/OneUptime/oneuptime
synced 2024-11-22 23:30:10 +00:00
934 lines
29 KiB
TypeScript
934 lines
29 KiB
TypeScript
import SubscriptionPlan from 'Common/Types/Billing/SubscriptionPlan';
|
|
import MeteredPlan from 'Common/Types/Billing/MeteredPlan';
|
|
import OneUptimeDate from 'Common/Types/Date';
|
|
import APIException from 'Common/Types/Exception/ApiException';
|
|
import BadDataException from 'Common/Types/Exception/BadDataException';
|
|
import ObjectID from 'Common/Types/ObjectID';
|
|
import logger from '../Utils/Logger';
|
|
import Stripe from 'stripe';
|
|
import { BillingPrivateKey, IsBillingEnabled } from '../EnvironmentConfig';
|
|
import ServerMeteredPlan from '../Types/Billing/MeteredPlan/ServerMeteredPlan';
|
|
import SubscriptionStatus, {
|
|
SubscriptionStatusUtil,
|
|
} from 'Common/Types/Billing/SubscriptionStatus';
|
|
import BaseService from './BaseService';
|
|
import Email from 'Common/Types/Email';
|
|
import Dictionary from 'Common/Types/Dictionary';
|
|
import Errors from '../Utils/Errors';
|
|
import ProjectService from './ProjectService';
|
|
import { ProductType } from 'Model/Models/UsageBilling';
|
|
|
|
export type SubscriptionItem = Stripe.SubscriptionItem;
|
|
|
|
export type Coupon = Stripe.Coupon;
|
|
|
|
export interface PaymentMethod {
|
|
id: string;
|
|
type: string;
|
|
last4Digits: string;
|
|
isDefault: boolean;
|
|
}
|
|
|
|
export interface Invoice {
|
|
id: string;
|
|
amount: number;
|
|
currencyCode: string;
|
|
subscriptionId?: string | undefined;
|
|
status: string;
|
|
downloadableLink: string;
|
|
customerId: string | undefined;
|
|
}
|
|
|
|
export class BillingService extends BaseService {
|
|
public constructor() {
|
|
super();
|
|
}
|
|
|
|
private stripe: Stripe = new Stripe(BillingPrivateKey, {
|
|
apiVersion: '2022-08-01',
|
|
});
|
|
|
|
// returns billing id of the customer.
|
|
public async createCustomer(data: {
|
|
name: string;
|
|
id: ObjectID;
|
|
email: Email;
|
|
}): Promise<string> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
const customer: Stripe.Response<Stripe.Customer> =
|
|
await this.stripe.customers.create({
|
|
name: data.name,
|
|
email: data.email.toString(),
|
|
metadata: {
|
|
id: data.id.toString(),
|
|
},
|
|
});
|
|
return customer.id;
|
|
}
|
|
|
|
public async updateCustomerName(
|
|
id: string,
|
|
newName: string
|
|
): Promise<void> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
await this.stripe.customers.update(id, { name: newName });
|
|
}
|
|
|
|
public async deleteCustomer(id: string): Promise<void> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
await this.stripe.customers.del(id);
|
|
}
|
|
|
|
public isBillingEnabled(): boolean {
|
|
return IsBillingEnabled;
|
|
}
|
|
|
|
public isSubscriptionActive(status: SubscriptionStatus): boolean {
|
|
return SubscriptionStatusUtil.isSubscriptionActive(status);
|
|
}
|
|
|
|
public async subscribeToMeteredPlan(data: {
|
|
projectId: ObjectID;
|
|
customerId: string;
|
|
serverMeteredPlans: Array<ServerMeteredPlan>;
|
|
trialDate: Date | null;
|
|
defaultPaymentMethodId?: string | undefined;
|
|
promoCode?: string | undefined;
|
|
}): Promise<{
|
|
meteredSubscriptionId: string;
|
|
trialEndsAt: Date | null;
|
|
}> {
|
|
const meteredPlanSubscriptionParams: Stripe.SubscriptionCreateParams = {
|
|
customer: data.customerId,
|
|
|
|
proration_behavior: 'always_invoice',
|
|
|
|
items: data.serverMeteredPlans.map((item: ServerMeteredPlan) => {
|
|
return {
|
|
price: item.getPriceId(),
|
|
};
|
|
}),
|
|
|
|
trial_end:
|
|
data.trialDate && OneUptimeDate.isInTheFuture(data.trialDate)
|
|
? OneUptimeDate.toUnixTimestamp(data.trialDate)
|
|
: 'now',
|
|
};
|
|
|
|
if (data.promoCode) {
|
|
meteredPlanSubscriptionParams.coupon = data.promoCode;
|
|
}
|
|
|
|
if (data.defaultPaymentMethodId) {
|
|
meteredPlanSubscriptionParams.default_payment_method =
|
|
data.defaultPaymentMethodId;
|
|
}
|
|
|
|
// Create metered subscriptions
|
|
const meteredSubscription: Stripe.Response<Stripe.Subscription> =
|
|
await this.stripe.subscriptions.create(
|
|
meteredPlanSubscriptionParams
|
|
);
|
|
|
|
for (const serverMeteredPlan of data.serverMeteredPlans) {
|
|
await serverMeteredPlan.reportQuantityToBillingProvider(
|
|
data.projectId,
|
|
{
|
|
meteredPlanSubscriptionId: meteredSubscription.id,
|
|
}
|
|
);
|
|
}
|
|
|
|
return {
|
|
meteredSubscriptionId: meteredSubscription.id,
|
|
trialEndsAt: data.trialDate,
|
|
};
|
|
}
|
|
|
|
public isTestEnvironment(): boolean {
|
|
return BillingPrivateKey.startsWith('sk_test');
|
|
}
|
|
|
|
public async generateCouponCode(data: {
|
|
name: string;
|
|
metadata?: Dictionary<string> | undefined;
|
|
percentOff: number;
|
|
durationInMonths: number;
|
|
maxRedemptions: number;
|
|
}): Promise<string> {
|
|
const coupon: Coupon = await this.stripe.coupons.create({
|
|
name: data.name,
|
|
percent_off: data.percentOff,
|
|
duration: 'repeating',
|
|
duration_in_months: data.durationInMonths,
|
|
max_redemptions: data.maxRedemptions,
|
|
metadata: data.metadata || null,
|
|
});
|
|
|
|
return coupon.id;
|
|
}
|
|
|
|
public async subscribeToPlan(data: {
|
|
projectId: ObjectID;
|
|
customerId: string;
|
|
serverMeteredPlans: Array<ServerMeteredPlan>;
|
|
plan: SubscriptionPlan;
|
|
quantity: number;
|
|
isYearly: boolean;
|
|
trial: boolean | Date | undefined;
|
|
defaultPaymentMethodId?: string | undefined;
|
|
promoCode?: string | undefined;
|
|
}): Promise<{
|
|
subscriptionId: string;
|
|
meteredSubscriptionId: string;
|
|
trialEndsAt: Date | null;
|
|
}> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
let trialDate: Date | null = null;
|
|
|
|
if (typeof data.trial === 'boolean') {
|
|
if (data.trial) {
|
|
trialDate = OneUptimeDate.getSomeDaysAfter(
|
|
data.plan.getTrialPeriod()
|
|
);
|
|
} else {
|
|
trialDate = null;
|
|
}
|
|
} else if (data.trial instanceof Date) {
|
|
trialDate = data.trial;
|
|
}
|
|
|
|
const subscriptionParams: Stripe.SubscriptionCreateParams = {
|
|
customer: data.customerId,
|
|
|
|
items: [
|
|
{
|
|
price: data.isYearly
|
|
? data.plan.getYearlyPlanId()
|
|
: data.plan.getMonthlyPlanId(),
|
|
quantity: data.quantity,
|
|
},
|
|
],
|
|
|
|
proration_behavior: 'always_invoice',
|
|
|
|
trial_end:
|
|
trialDate && data.plan.getTrialPeriod() > 0
|
|
? OneUptimeDate.toUnixTimestamp(trialDate)
|
|
: 'now',
|
|
};
|
|
|
|
if (data.promoCode) {
|
|
subscriptionParams.coupon = data.promoCode;
|
|
}
|
|
|
|
if (data.defaultPaymentMethodId) {
|
|
subscriptionParams.default_payment_method =
|
|
data.defaultPaymentMethodId;
|
|
}
|
|
|
|
const subscription: Stripe.Response<Stripe.Subscription> =
|
|
await this.stripe.subscriptions.create(subscriptionParams);
|
|
|
|
// Create metered subscriptions
|
|
const meteredSubscription: {
|
|
meteredSubscriptionId: string;
|
|
trialEndsAt: Date | null;
|
|
} = await this.subscribeToMeteredPlan({
|
|
...data,
|
|
trialDate,
|
|
});
|
|
|
|
return {
|
|
subscriptionId: subscription.id,
|
|
meteredSubscriptionId: meteredSubscription.meteredSubscriptionId,
|
|
trialEndsAt:
|
|
trialDate && data.plan.getTrialPeriod() > 0 ? trialDate : null,
|
|
};
|
|
}
|
|
|
|
public async changeQuantity(
|
|
subscriptionId: string,
|
|
quantity: number
|
|
): Promise<void> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
const subscription: Stripe.Response<Stripe.Subscription> =
|
|
await this.stripe.subscriptions.retrieve(subscriptionId);
|
|
|
|
if (!subscription) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.SUBSCRIPTION_NOT_FOUND
|
|
);
|
|
}
|
|
if (subscription.status === 'canceled') {
|
|
// subscription is canceled.
|
|
return;
|
|
}
|
|
|
|
const subscriptionItemId: string | undefined =
|
|
subscription.items.data[0]?.id;
|
|
|
|
if (!subscriptionItemId) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.SUBSCRIPTION_ITEM_NOT_FOUND
|
|
);
|
|
}
|
|
|
|
await this.stripe.subscriptionItems.update(subscriptionItemId, {
|
|
quantity: quantity,
|
|
});
|
|
|
|
// add billing anchor, so that the billing cycle starts now. New quantity will be charged from now. https://stackoverflow.com/questions/44417047/immediately-charge-for-subscription-changes
|
|
await this.stripe.subscriptions.update(subscriptionId, {
|
|
proration_behavior: 'always_invoice',
|
|
});
|
|
}
|
|
|
|
public async addOrUpdateMeteredPricingOnSubscription(
|
|
subscriptionId: string,
|
|
serverMeteredPlan: ServerMeteredPlan,
|
|
quantity: number
|
|
): Promise<void> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
// get subscription.
|
|
const subscription: Stripe.Subscription = await this.getSubscription(
|
|
subscriptionId.toString()
|
|
);
|
|
|
|
if (!subscription) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.SUBSCRIPTION_NOT_FOUND
|
|
);
|
|
}
|
|
|
|
// check if this pricing exists
|
|
|
|
const pricingExists: boolean = subscription.items.data.some(
|
|
(item: SubscriptionItem) => {
|
|
return item.price?.id === serverMeteredPlan.getPriceId();
|
|
}
|
|
);
|
|
|
|
if (pricingExists) {
|
|
// update the quantity.
|
|
const subscriptionItemId: string | undefined =
|
|
subscription.items.data.find((item: SubscriptionItem) => {
|
|
return item.price?.id === serverMeteredPlan.getPriceId();
|
|
})?.id;
|
|
|
|
if (!subscriptionItemId) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.SUBSCRIPTION_ITEM_NOT_FOUND
|
|
);
|
|
}
|
|
|
|
// use stripe usage based api to update the quantity.
|
|
await this.stripe.subscriptionItems.createUsageRecord(
|
|
subscriptionItemId,
|
|
{
|
|
quantity: quantity,
|
|
}
|
|
);
|
|
} else {
|
|
// add the pricing.
|
|
const subscriptionItem: SubscriptionItem =
|
|
await this.stripe.subscriptionItems.create({
|
|
subscription: subscriptionId,
|
|
price: serverMeteredPlan.getPriceId(),
|
|
});
|
|
|
|
// use stripe usage based api to update the quantity.
|
|
await this.stripe.subscriptionItems.createUsageRecord(
|
|
subscriptionItem.id,
|
|
{
|
|
quantity: quantity,
|
|
}
|
|
);
|
|
}
|
|
|
|
// complete.
|
|
}
|
|
|
|
public async isPromoCodeValid(promoCode: string): Promise<boolean> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
try {
|
|
const promoCodeResponse: Stripe.Response<Stripe.Coupon> =
|
|
await this.stripe.coupons.retrieve(promoCode);
|
|
|
|
if (!promoCodeResponse) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.PROMO_CODE_NOT_FOUND
|
|
);
|
|
}
|
|
|
|
return promoCodeResponse.valid;
|
|
} catch (err) {
|
|
throw new BadDataException(
|
|
(err as Error).message ||
|
|
Errors.BillingService.PROMO_CODE_INVALID
|
|
);
|
|
}
|
|
}
|
|
|
|
public async removeSubscriptionItem(
|
|
subscriptionId: string,
|
|
subscriptionItemId: string,
|
|
isMeteredSubscriptionItem: boolean
|
|
): Promise<void> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
const subscription: Stripe.Response<Stripe.Subscription> =
|
|
await this.stripe.subscriptions.retrieve(subscriptionId);
|
|
|
|
if (!subscription) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.SUBSCRIPTION_NOT_FOUND
|
|
);
|
|
}
|
|
|
|
if (subscription.status === 'canceled') {
|
|
// subscription is canceled.
|
|
return;
|
|
}
|
|
|
|
const subscriptionItemOptions: Stripe.SubscriptionItemDeleteParams =
|
|
isMeteredSubscriptionItem
|
|
? {
|
|
proration_behavior: 'create_prorations',
|
|
clear_usage: true,
|
|
}
|
|
: {};
|
|
|
|
await this.stripe.subscriptionItems.del(
|
|
subscriptionItemId,
|
|
subscriptionItemOptions
|
|
);
|
|
}
|
|
|
|
public async getSubscriptionItems(
|
|
subscriptionId: string
|
|
): Promise<Array<SubscriptionItem>> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
const subscription: Stripe.Response<Stripe.Subscription> =
|
|
await this.stripe.subscriptions.retrieve(subscriptionId);
|
|
|
|
if (!subscription) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.SUBSCRIPTION_NOT_FOUND
|
|
);
|
|
}
|
|
|
|
return subscription.items.data;
|
|
}
|
|
|
|
public async changePlan(data: {
|
|
projectId: ObjectID;
|
|
subscriptionId: string;
|
|
meteredSubscriptionId: string;
|
|
serverMeteredPlans: Array<ServerMeteredPlan>;
|
|
newPlan: SubscriptionPlan;
|
|
quantity: number;
|
|
isYearly: boolean;
|
|
endTrialAt?: Date | undefined;
|
|
}): Promise<{
|
|
subscriptionId: string;
|
|
meteredSubscriptionId: string;
|
|
trialEndsAt?: Date | undefined;
|
|
}> {
|
|
logger.info('Changing plan');
|
|
logger.info(data);
|
|
|
|
if (!this.isBillingEnabled()) {
|
|
logger.info(Errors.BillingService.BILLING_NOT_ENABLED);
|
|
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
const subscription: Stripe.Response<Stripe.Subscription> =
|
|
await this.stripe.subscriptions.retrieve(data.subscriptionId);
|
|
|
|
logger.info('Subscription');
|
|
logger.info(subscription);
|
|
|
|
if (!subscription) {
|
|
logger.info(Errors.BillingService.SUBSCRIPTION_NOT_FOUND);
|
|
throw new BadDataException(
|
|
Errors.BillingService.SUBSCRIPTION_NOT_FOUND
|
|
);
|
|
}
|
|
|
|
logger.info('Subscription status');
|
|
logger.info(subscription.status);
|
|
|
|
const paymentMethods: Array<PaymentMethod> =
|
|
await this.getPaymentMethods(subscription.customer.toString());
|
|
|
|
logger.info('Payment methods');
|
|
logger.info(paymentMethods);
|
|
|
|
if (paymentMethods.length === 0) {
|
|
logger.info('No payment methods');
|
|
|
|
throw new BadDataException(
|
|
Errors.BillingService.NO_PAYMENTS_METHODS
|
|
);
|
|
}
|
|
|
|
logger.info('Cancelling subscriptions');
|
|
logger.info(data.subscriptionId);
|
|
await this.cancelSubscription(data.subscriptionId);
|
|
|
|
logger.info('Cancelling metered subscriptions');
|
|
logger.info(data.meteredSubscriptionId);
|
|
await this.cancelSubscription(data.meteredSubscriptionId);
|
|
|
|
if (data.endTrialAt && !OneUptimeDate.isInTheFuture(data.endTrialAt)) {
|
|
data.endTrialAt = undefined;
|
|
}
|
|
|
|
logger.info('Subscribing to plan');
|
|
|
|
const subscribeToPlan: {
|
|
subscriptionId: string;
|
|
meteredSubscriptionId: string;
|
|
trialEndsAt: Date | null;
|
|
} = await this.subscribeToPlan({
|
|
projectId: data.projectId,
|
|
customerId: subscription.customer.toString(),
|
|
serverMeteredPlans: data.serverMeteredPlans,
|
|
plan: data.newPlan,
|
|
quantity: data.quantity,
|
|
isYearly: data.isYearly,
|
|
trial: data.endTrialAt,
|
|
defaultPaymentMethodId: paymentMethods[0]?.id,
|
|
promoCode: undefined,
|
|
});
|
|
|
|
logger.info('Subscribed to plan');
|
|
|
|
const value: {
|
|
subscriptionId: string;
|
|
meteredSubscriptionId: string;
|
|
trialEndsAt?: Date | undefined;
|
|
} = {
|
|
subscriptionId: subscribeToPlan.subscriptionId,
|
|
meteredSubscriptionId: subscribeToPlan.meteredSubscriptionId,
|
|
trialEndsAt: subscribeToPlan.trialEndsAt || undefined,
|
|
};
|
|
|
|
return value;
|
|
}
|
|
|
|
public async deletePaymentMethod(
|
|
customerId: string,
|
|
paymentMethodId: string
|
|
): Promise<void> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
const paymentMethods: Array<PaymentMethod> =
|
|
await this.getPaymentMethods(customerId);
|
|
|
|
if (paymentMethods.length === 1) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.MIN_REQUIRED_PAYMENT_METHOD_NOT_MET
|
|
);
|
|
}
|
|
|
|
await this.stripe.paymentMethods.detach(paymentMethodId);
|
|
}
|
|
|
|
public async hasPaymentMethods(customerId: string): Promise<boolean> {
|
|
if ((await this.getPaymentMethods(customerId)).length > 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public async setDefaultPaymentMethod(
|
|
customerId: string,
|
|
paymentMethodId: string
|
|
): Promise<void> {
|
|
await this.stripe.customers.update(customerId, {
|
|
invoice_settings: {
|
|
default_payment_method: paymentMethodId,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async getPaymentMethods(
|
|
customerId: string
|
|
): Promise<Array<PaymentMethod>> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
const paymentMethods: Array<PaymentMethod> = [];
|
|
|
|
const cardPaymentMethods: Stripe.ApiList<Stripe.PaymentMethod> =
|
|
await this.stripe.paymentMethods.list({
|
|
customer: customerId,
|
|
type: 'card',
|
|
});
|
|
|
|
const sepaPaymentMethods: Stripe.ApiList<Stripe.PaymentMethod> =
|
|
await this.stripe.paymentMethods.list({
|
|
customer: customerId,
|
|
type: 'sepa_debit',
|
|
});
|
|
|
|
const usBankPaymentMethods: Stripe.ApiList<Stripe.PaymentMethod> =
|
|
await this.stripe.paymentMethods.list({
|
|
customer: customerId,
|
|
type: 'us_bank_account',
|
|
});
|
|
|
|
const bacsPaymentMethods: Stripe.ApiList<Stripe.PaymentMethod> =
|
|
await this.stripe.paymentMethods.list({
|
|
customer: customerId,
|
|
type: 'bacs_debit',
|
|
});
|
|
|
|
cardPaymentMethods.data.forEach((item: Stripe.PaymentMethod) => {
|
|
paymentMethods.push({
|
|
type: item.card?.brand || 'Card',
|
|
last4Digits: item.card?.last4 || 'xxxx',
|
|
isDefault: false,
|
|
id: item.id,
|
|
});
|
|
});
|
|
|
|
bacsPaymentMethods.data.forEach((item: Stripe.PaymentMethod) => {
|
|
paymentMethods.push({
|
|
type: 'UK Bank Account',
|
|
last4Digits: item.bacs_debit?.last4 || 'xxxx',
|
|
isDefault: false,
|
|
id: item.id,
|
|
});
|
|
});
|
|
|
|
usBankPaymentMethods.data.forEach((item: Stripe.PaymentMethod) => {
|
|
paymentMethods.push({
|
|
type: 'US Bank Account',
|
|
last4Digits: item.us_bank_account?.last4 || 'xxxx',
|
|
isDefault: false,
|
|
id: item.id,
|
|
});
|
|
});
|
|
|
|
sepaPaymentMethods.data.forEach((item: Stripe.PaymentMethod) => {
|
|
paymentMethods.push({
|
|
type: 'EU Bank Account',
|
|
last4Digits: item.sepa_debit?.last4 || 'xxxx',
|
|
isDefault: false,
|
|
id: item.id,
|
|
});
|
|
});
|
|
|
|
// check if there's a default payment method.
|
|
|
|
const customer: Stripe.Response<
|
|
Stripe.Customer | Stripe.DeletedCustomer
|
|
> = await this.stripe.customers.retrieve(customerId);
|
|
|
|
if (
|
|
(customer as Stripe.Customer).invoice_settings &&
|
|
!(customer as Stripe.Customer).invoice_settings
|
|
?.default_payment_method
|
|
) {
|
|
// set the first payment method as default.
|
|
if (paymentMethods.length > 0 && paymentMethods[0]?.id) {
|
|
await this.setDefaultPaymentMethod(
|
|
customerId,
|
|
paymentMethods[0]?.id
|
|
);
|
|
}
|
|
}
|
|
|
|
return paymentMethods;
|
|
}
|
|
|
|
public async getSetupIntentSecret(customerId: string): Promise<string> {
|
|
const setupIntent: Stripe.Response<Stripe.SetupIntent> =
|
|
await this.stripe.setupIntents.create({
|
|
customer: customerId,
|
|
});
|
|
|
|
if (!setupIntent.client_secret) {
|
|
throw new APIException(Errors.BillingService.CLIENT_SECRET_MISSING);
|
|
}
|
|
|
|
return setupIntent.client_secret;
|
|
}
|
|
|
|
public async cancelSubscription(subscriptionId: string): Promise<void> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
try {
|
|
await this.stripe.subscriptions.del(subscriptionId);
|
|
} catch (err) {
|
|
logger.error(err);
|
|
}
|
|
}
|
|
|
|
public async getSubscriptionStatus(
|
|
subscriptionId: string
|
|
): Promise<SubscriptionStatus> {
|
|
const subscription: Stripe.Subscription = await this.getSubscription(
|
|
subscriptionId
|
|
);
|
|
return subscription.status as SubscriptionStatus;
|
|
}
|
|
|
|
public async getSubscription(
|
|
subscriptionId: string
|
|
): Promise<Stripe.Subscription> {
|
|
if (!this.isBillingEnabled()) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.BILLING_NOT_ENABLED
|
|
);
|
|
}
|
|
|
|
const subscription: Stripe.Response<Stripe.Subscription> =
|
|
await this.stripe.subscriptions.retrieve(subscriptionId);
|
|
|
|
return subscription;
|
|
}
|
|
|
|
public async getInvoices(customerId: string): Promise<Array<Invoice>> {
|
|
const invoices: Stripe.ApiList<Stripe.Invoice> =
|
|
await this.stripe.invoices.list({
|
|
customer: customerId,
|
|
limit: 100,
|
|
});
|
|
|
|
return invoices.data.map((invoice: Stripe.Invoice) => {
|
|
return {
|
|
id: invoice.id!,
|
|
amount: invoice.amount_due,
|
|
currencyCode: invoice.currency,
|
|
subscriptionId: invoice.subscription?.toString() || undefined,
|
|
status: invoice.status?.toString() || 'Unknown',
|
|
downloadableLink: invoice.invoice_pdf?.toString() || '',
|
|
customerId: invoice.customer?.toString() || '',
|
|
};
|
|
});
|
|
}
|
|
|
|
public async generateInvoiceAndChargeCustomer(
|
|
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(Errors.BillingService.INVOICE_NOT_GENERATED);
|
|
}
|
|
|
|
await this.stripe.invoiceItems.create({
|
|
invoice: invoice.id,
|
|
amount: amountInUsd * 100,
|
|
description: itemText,
|
|
customer: customerId,
|
|
});
|
|
|
|
await this.stripe.invoices.finalizeInvoice(invoice.id!);
|
|
|
|
try {
|
|
await this.payInvoice(customerId, invoice.id!);
|
|
} catch (err) {
|
|
// mark invoice as failed and do not collect payment.
|
|
await this.voidInvoice(invoice.id!);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
public async voidInvoice(invoiceId: string): Promise<Stripe.Invoice> {
|
|
const invoice: Stripe.Invoice = await this.stripe.invoices.voidInvoice(
|
|
invoiceId
|
|
);
|
|
|
|
return invoice;
|
|
}
|
|
|
|
public async payInvoice(
|
|
customerId: string,
|
|
invoiceId: string
|
|
): Promise<Invoice> {
|
|
// after the invoice is paid, // please fetch subscription and check the status.
|
|
const paymentMethods: Array<PaymentMethod> =
|
|
await this.getPaymentMethods(customerId);
|
|
|
|
if (paymentMethods.length === 0) {
|
|
throw new BadDataException(
|
|
Errors.BillingService.NO_PAYMENTS_METHODS
|
|
);
|
|
}
|
|
|
|
const invoice: Stripe.Invoice = await this.stripe.invoices.pay(
|
|
invoiceId,
|
|
{
|
|
payment_method: paymentMethods[0]?.id || '',
|
|
}
|
|
);
|
|
|
|
return {
|
|
id: invoice.id!,
|
|
amount: invoice.amount_due,
|
|
currencyCode: invoice.currency,
|
|
subscriptionId: invoice.subscription?.toString() || undefined,
|
|
status: invoice.status?.toString() || 'Unknown',
|
|
downloadableLink: invoice.invoice_pdf?.toString() || '',
|
|
customerId: invoice.customer?.toString() || '',
|
|
};
|
|
}
|
|
|
|
public getMeteredPlanPriceId(productType: ProductType): string {
|
|
if (productType === ProductType.ActiveMonitoring) {
|
|
if (this.isTestEnvironment()) {
|
|
return 'price_1N6CHFANuQdJ93r7qDaLmb7S';
|
|
}
|
|
|
|
return 'price_1N6B9EANuQdJ93r7fj3bhcWP';
|
|
}
|
|
|
|
if (productType === ProductType.Logs) {
|
|
if (this.isTestEnvironment()) {
|
|
return 'price_1OPnB5ANuQdJ93r7jG4NLCJG';
|
|
}
|
|
|
|
return 'price_1OQ8gwANuQdJ93r74Pi85UQq';
|
|
}
|
|
|
|
if (productType === ProductType.Traces) {
|
|
if (this.isTestEnvironment()) {
|
|
return 'price_1OQ8i9ANuQdJ93r75J3wr0PX';
|
|
}
|
|
|
|
return 'price_1OQ8ivANuQdJ93r7NAR8KbH3';
|
|
}
|
|
|
|
if (productType === ProductType.Metrics) {
|
|
if (this.isTestEnvironment()) {
|
|
return 'price_1OQ8iqANuQdJ93r7wZ7gJ7Gb';
|
|
}
|
|
|
|
return 'price_1OQ8j0ANuQdJ93r7WGzR0p6j';
|
|
}
|
|
|
|
throw new BadDataException(
|
|
'Plan with productType ' + productType + ' not found'
|
|
);
|
|
}
|
|
|
|
public async getMeteredPlan(data: {
|
|
productType: ProductType;
|
|
projectId: ObjectID;
|
|
}): Promise<MeteredPlan> {
|
|
if (data.productType === ProductType.ActiveMonitoring) {
|
|
return new MeteredPlan({
|
|
priceId: this.getMeteredPlanPriceId(data.productType),
|
|
pricePerUnitInUSD: 1,
|
|
unitName: 'Active Monitor',
|
|
});
|
|
}
|
|
|
|
const dataRetentionDays: number =
|
|
await ProjectService.getTelemetryDataRetentionInDays(
|
|
data.projectId
|
|
);
|
|
|
|
const dataRetentionMultiplier: number = 0.1; // if the retention is 10 days for example, the cost per GB will be 0.01$ per GB per day (0.10 * dataRetentionDays * dataRetentionMultiplier).
|
|
|
|
if (data.productType === ProductType.Logs) {
|
|
return new MeteredPlan({
|
|
priceId: this.getMeteredPlanPriceId(data.productType),
|
|
pricePerUnitInUSD:
|
|
0.1 * dataRetentionDays * dataRetentionMultiplier,
|
|
unitName: `GB (${dataRetentionDays} days data retention)`,
|
|
});
|
|
}
|
|
|
|
if (data.productType === ProductType.Traces) {
|
|
return new MeteredPlan({
|
|
priceId: this.getMeteredPlanPriceId(data.productType),
|
|
pricePerUnitInUSD:
|
|
0.1 * dataRetentionDays * dataRetentionMultiplier,
|
|
unitName: `GB (${dataRetentionDays} days data retention)`,
|
|
});
|
|
}
|
|
|
|
if (data.productType === ProductType.Metrics) {
|
|
return new MeteredPlan({
|
|
priceId: this.getMeteredPlanPriceId(data.productType),
|
|
pricePerUnitInUSD:
|
|
0.1 * dataRetentionDays * dataRetentionMultiplier,
|
|
unitName: `GB (${dataRetentionDays} days data retention)`,
|
|
});
|
|
}
|
|
|
|
throw new BadDataException(
|
|
'Plan with name ' + data.productType + ' not found'
|
|
);
|
|
}
|
|
}
|
|
|
|
export default new BillingService();
|