mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
b312955838
* Add subscription start, trial end, and end dates to db * Add subscription start, trial end, and end date to db * Write subscription start date on new team subscriptions * Write subscription start date for new orgs * Fix typo in stripe billing service file (billling -> billing) * Use `StripeBillingService.extractSubscriptionDates` * Remove comments * Address comment * Fix typo in file import * Fix typo in file import * Add missing SubscriptionStatus enum values - Add INCOMPLETE, INCOMPLETE_EXPIRED, UNPAID, PAUSED enum values - These values are referenced in stripe-billing-service.ts status mapping - Fixes type errors in billing-related code Co-Authored-By: [email protected] <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
245 lines
7.1 KiB
TypeScript
245 lines
7.1 KiB
TypeScript
import Stripe from "stripe";
|
|
|
|
import logger from "@calcom/lib/logger";
|
|
|
|
import type { BillingService } from "./billing-service";
|
|
import { SubscriptionStatus } from "./repository/IBillingRepository";
|
|
|
|
export class StripeBillingService implements BillingService {
|
|
private stripe: Stripe;
|
|
constructor() {
|
|
this.stripe = new Stripe(process.env.STRIPE_PRIVATE_KEY!, {
|
|
apiVersion: "2020-08-27",
|
|
});
|
|
}
|
|
|
|
async createCustomer(args: Parameters<BillingService["createCustomer"]>[0]) {
|
|
const { email, metadata } = args;
|
|
const customer = await this.stripe.customers.create({
|
|
email,
|
|
metadata: {
|
|
email,
|
|
...metadata,
|
|
},
|
|
});
|
|
return { stripeCustomerId: customer.id };
|
|
}
|
|
|
|
async createPaymentIntent(args: Parameters<BillingService["createPaymentIntent"]>[0]) {
|
|
const { customerId, amount, metadata } = args;
|
|
const paymentIntent = await this.stripe.paymentIntents.create({
|
|
customer: customerId,
|
|
amount,
|
|
currency: "usd",
|
|
automatic_payment_methods: {
|
|
enabled: true,
|
|
},
|
|
metadata,
|
|
});
|
|
|
|
return {
|
|
id: paymentIntent.id,
|
|
client_secret: paymentIntent.client_secret,
|
|
};
|
|
}
|
|
|
|
async createOneTimeCheckout(args: {
|
|
priceId: string;
|
|
quantity: number;
|
|
successUrl: string;
|
|
cancelUrl: string;
|
|
metadata?: Record<string, string>;
|
|
}) {
|
|
const { priceId, quantity, successUrl, cancelUrl, metadata } = args;
|
|
|
|
const session = await this.stripe.checkout.sessions.create({
|
|
line_items: [{ price: priceId, quantity }],
|
|
mode: "payment",
|
|
success_url: successUrl,
|
|
cancel_url: cancelUrl,
|
|
metadata: metadata,
|
|
invoice_creation: {
|
|
enabled: true,
|
|
},
|
|
// eslint-disable-next-line
|
|
} as any);
|
|
|
|
return {
|
|
checkoutUrl: session.url,
|
|
sessionId: session.id,
|
|
};
|
|
}
|
|
|
|
async createSubscriptionCheckout(args: Parameters<BillingService["createSubscriptionCheckout"]>[0]) {
|
|
const {
|
|
customerId,
|
|
successUrl,
|
|
cancelUrl,
|
|
priceId,
|
|
quantity,
|
|
metadata,
|
|
mode = "subscription",
|
|
allowPromotionCodes = true,
|
|
customerUpdate,
|
|
automaticTax,
|
|
discounts,
|
|
subscriptionData,
|
|
} = args;
|
|
|
|
const session = await this.stripe.checkout.sessions.create({
|
|
customer: customerId,
|
|
success_url: successUrl,
|
|
cancel_url: cancelUrl,
|
|
mode,
|
|
metadata,
|
|
line_items: [
|
|
{
|
|
price: priceId,
|
|
quantity,
|
|
},
|
|
],
|
|
allow_promotion_codes: allowPromotionCodes,
|
|
customer_update: customerUpdate,
|
|
automatic_tax: automaticTax,
|
|
discounts,
|
|
subscription_data: subscriptionData,
|
|
});
|
|
|
|
return {
|
|
checkoutUrl: session.url,
|
|
sessionId: session.id,
|
|
};
|
|
}
|
|
|
|
async createPrice(args: Parameters<BillingService["createPrice"]>[0]) {
|
|
const { amount, currency, interval, productId, nickname, metadata } = args;
|
|
|
|
const price = await this.stripe.prices.create({
|
|
nickname,
|
|
unit_amount: amount,
|
|
currency,
|
|
recurring: {
|
|
interval,
|
|
},
|
|
product: productId,
|
|
metadata,
|
|
});
|
|
|
|
return {
|
|
priceId: price.id,
|
|
};
|
|
}
|
|
|
|
async handleSubscriptionCreation(subscriptionId: string) {
|
|
throw new Error(`Method not implemented for subscription id ${subscriptionId}`);
|
|
}
|
|
|
|
async handleSubscriptionCancel(subscriptionId: string) {
|
|
await this.stripe.subscriptions.cancel(subscriptionId);
|
|
}
|
|
|
|
async handleSubscriptionUpdate(args: Parameters<BillingService["handleSubscriptionUpdate"]>[0]) {
|
|
const { subscriptionId, subscriptionItemId, membershipCount } = args;
|
|
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
|
|
const subscriptionQuantity = subscription.items.data.find(
|
|
(sub) => sub.id === subscriptionItemId
|
|
)?.quantity;
|
|
if (!subscriptionQuantity) throw new Error("Subscription not found");
|
|
await this.stripe.subscriptions.update(subscriptionId, {
|
|
items: [{ quantity: membershipCount, id: subscriptionItemId }],
|
|
});
|
|
}
|
|
|
|
async handleEndTrial(subscriptionId: string) {
|
|
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
|
|
|
|
if (subscription.status !== "trialing") {
|
|
return; // Do nothing if not in trial
|
|
}
|
|
|
|
await this.stripe.subscriptions.update(subscriptionId, {
|
|
trial_end: "now",
|
|
});
|
|
}
|
|
|
|
async checkoutSessionIsPaid(paymentId: string) {
|
|
const checkoutSession = await this.stripe.checkout.sessions.retrieve(paymentId);
|
|
return checkoutSession.payment_status === "paid";
|
|
}
|
|
async getSubscriptionStatus(subscriptionId: string) {
|
|
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
|
|
if (!subscription || !subscription.status) return null;
|
|
|
|
return subscription.status;
|
|
}
|
|
|
|
async getCheckoutSession(checkoutSessionId: string) {
|
|
const checkoutSession = await this.stripe.checkout.sessions.retrieve(checkoutSessionId);
|
|
return checkoutSession;
|
|
}
|
|
|
|
async getCustomer(customerId: string) {
|
|
const customer = await this.stripe.customers.retrieve(customerId);
|
|
return customer;
|
|
}
|
|
|
|
async getSubscriptions(customerId: string) {
|
|
const subscriptions = await this.stripe.subscriptions.list({ customer: customerId });
|
|
return subscriptions.data;
|
|
}
|
|
|
|
async updateCustomer(args: Parameters<BillingService["updateCustomer"]>[0]) {
|
|
const { customerId, email, userId } = args;
|
|
const metadata: { email?: string; userId?: number } = {};
|
|
if (email) metadata.email = email;
|
|
if (userId) metadata.userId = userId;
|
|
await this.stripe.customers.update(customerId, { metadata });
|
|
}
|
|
|
|
async getPrice(priceId: string) {
|
|
const price = await this.stripe.prices.retrieve(priceId);
|
|
return price;
|
|
}
|
|
|
|
static extractSubscriptionDates(subscription: {
|
|
start_date: number;
|
|
trial_end?: number | null;
|
|
cancel_at?: number | null;
|
|
}) {
|
|
// Stripe returns dates as unix time in seconds but Date() expects milliseconds
|
|
const subscriptionStart = new Date(subscription.start_date * 1000);
|
|
const subscriptionTrialEnd = subscription?.trial_end ? new Date(subscription.trial_end * 1000) : null;
|
|
const subscriptionEnd = subscription?.cancel_at ? new Date(subscription.cancel_at * 1000) : null;
|
|
|
|
return { subscriptionStart, subscriptionTrialEnd, subscriptionEnd };
|
|
}
|
|
|
|
static mapStripeStatusToCalStatus = ({
|
|
stripeStatus,
|
|
subscriptionId,
|
|
}: {
|
|
stripeStatus: string;
|
|
subscriptionId: string;
|
|
}) => {
|
|
const log = logger.getSubLogger({ prefix: ["mapStripeStatusToCalStatus"] });
|
|
const statusMap: Record<string, SubscriptionStatus> = {
|
|
active: SubscriptionStatus.ACTIVE,
|
|
past_due: SubscriptionStatus.PAST_DUE,
|
|
canceled: SubscriptionStatus.CANCELLED,
|
|
cancelled: SubscriptionStatus.CANCELLED,
|
|
trialing: SubscriptionStatus.TRIALING,
|
|
incomplete: SubscriptionStatus.INCOMPLETE,
|
|
incomplete_expired: SubscriptionStatus.INCOMPLETE_EXPIRED,
|
|
unpaid: SubscriptionStatus.UNPAID,
|
|
paused: SubscriptionStatus.PAUSED,
|
|
};
|
|
|
|
const status = statusMap[stripeStatus];
|
|
if (!status) {
|
|
log.warn(`Unhandled status for ${stripeStatus} and sub id ${subscriptionId}`);
|
|
}
|
|
|
|
return status || SubscriptionStatus.ACTIVE;
|
|
};
|
|
}
|