feat: Add subscription start, trial end, and dates to billing tables (#24408)
* 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: joe@cal.com <j.auyeung419@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
joe@cal.com
Devin AI
parent
003aaaa18b
commit
b312955838
@@ -5,6 +5,7 @@ import type Stripe from "stripe";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -57,6 +58,9 @@ async function handler(request: NextRequest) {
|
||||
});
|
||||
|
||||
if (checkoutSessionSubscription) {
|
||||
const { subscriptionStart } =
|
||||
StripeBillingService.extractSubscriptionDates(checkoutSessionSubscription);
|
||||
|
||||
const internalBillingService = new InternalTeamBilling(finalizedTeam);
|
||||
await internalBillingService.saveTeamBilling({
|
||||
teamId: finalizedTeam.id,
|
||||
@@ -66,6 +70,7 @@ async function handler(request: NextRequest) {
|
||||
// TODO: Implement true subscription status when webhook events are implemented
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
planName: Plan.TEAM,
|
||||
subscriptionStart,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type Stripe from "stripe";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -87,6 +88,7 @@ async function getHandler(req: NextRequest) {
|
||||
});
|
||||
|
||||
if (checkoutSession && subscription) {
|
||||
const { subscriptionStart } = StripeBillingService.extractSubscriptionDates(subscription);
|
||||
const internalBillingService = new InternalTeamBilling(team);
|
||||
await internalBillingService.saveTeamBilling({
|
||||
teamId: team.id,
|
||||
@@ -96,6 +98,7 @@ async function getHandler(req: NextRequest) {
|
||||
// TODO: Implement true subscription status when webhook events are implemented
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
planName: Plan.TEAM,
|
||||
subscriptionStart,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ vi.mock("@calcom/prisma", () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/features/ee/billing/stripe-billling-service", () => {
|
||||
vi.mock("@calcom/features/ee/billing/stripe-billing-service", () => {
|
||||
return {
|
||||
StripeBillingService: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { IS_STRIPE_ENABLED } from "@calcom/lib/constants";
|
||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
|
||||
export async function getCustomerAndCheckoutSession(checkoutSessionId: string) {
|
||||
const billingService = new StripeBillingService();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getLocaleFromRequest } from "@calcom/features/auth/lib/getLocaleFromReq
|
||||
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
||||
import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry";
|
||||
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
import { hashPassword } from "@calcom/lib/auth/hashPassword";
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
|
||||
import { createOrganizationFromOnboarding } from "@calcom/features/ee/organizations/lib/server/createOrganizationFromOnboarding";
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { OrganizationOnboardingRepository } from "@calcom/lib/server/repository/organizationOnboarding";
|
||||
@@ -96,6 +98,10 @@ const handler = async (data: SWHMap["invoice.paid"]["data"]) => {
|
||||
paymentSubscriptionItemId,
|
||||
});
|
||||
|
||||
// Get the Stripe subscription object
|
||||
const stripeSubscription = await stripe.subscriptions.retrieve(paymentSubscriptionId);
|
||||
const { subscriptionStart } = StripeBillingService.extractSubscriptionDates(stripeSubscription);
|
||||
|
||||
const internalTeamBillingService = new InternalTeamBilling(organization);
|
||||
await internalTeamBillingService.saveTeamBilling({
|
||||
teamId: organization.id,
|
||||
@@ -105,6 +111,7 @@ const handler = async (data: SWHMap["invoice.paid"]["data"]) => {
|
||||
// TODO: Write actual status when webhook events are added
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
planName: Plan.ORGANIZATION,
|
||||
subscriptionStart,
|
||||
});
|
||||
|
||||
logger.debug(`Marking onboarding as complete for organization ${organization.id}`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StripeBillingService } from "./stripe-billling-service";
|
||||
import { StripeBillingService } from "./stripe-billing-service";
|
||||
|
||||
export class BillingFactory {
|
||||
constructor() {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||
import { CreditType } from "@calcom/prisma/enums";
|
||||
|
||||
import { CreditService } from "./credit-service";
|
||||
import { StripeBillingService } from "./stripe-billling-service";
|
||||
import { StripeBillingService } from "./stripe-billing-service";
|
||||
import { InternalTeamBilling } from "./teams/internal-team-billing";
|
||||
|
||||
const MOCK_TX = {};
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
sendCreditBalanceLimitReachedEmails,
|
||||
sendCreditBalanceLowWarningEmails,
|
||||
} from "@calcom/emails/email-manager";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
|
||||
import { cancelScheduledMessagesAndScheduleEmails } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||
import { IS_SMS_CREDITS_ENABLED } from "@calcom/lib/constants";
|
||||
|
||||
@@ -9,6 +9,10 @@ export enum SubscriptionStatus {
|
||||
CANCELLED = "CANCELLED",
|
||||
PAST_DUE = "PAST_DUE",
|
||||
TRIALING = "TRIALING",
|
||||
INCOMPLETE = "INCOMPLETE",
|
||||
INCOMPLETE_EXPIRED = "INCOMPLETE_EXPIRED",
|
||||
UNPAID = "UNPAID",
|
||||
PAUSED = "PAUSED",
|
||||
}
|
||||
|
||||
export interface BillingRecord {
|
||||
@@ -37,4 +41,7 @@ export interface IBillingRepositoryCreateArgs {
|
||||
customerId: string;
|
||||
planName: Plan;
|
||||
status: SubscriptionStatus;
|
||||
subscriptionStart?: Date;
|
||||
subscriptionTrialEnd?: Date;
|
||||
subscriptionEnd?: Date;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import Stripe from "stripe";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { StripeBillingService } from "./stripe-billling-service";
|
||||
import { StripeBillingService } from "./stripe-billing-service";
|
||||
|
||||
vi.mock("stripe");
|
||||
|
||||
+46
-1
@@ -1,6 +1,9 @@
|
||||
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;
|
||||
@@ -58,6 +61,7 @@ export class StripeBillingService implements BillingService {
|
||||
invoice_creation: {
|
||||
enabled: true,
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
} as any);
|
||||
|
||||
return {
|
||||
@@ -127,7 +131,7 @@ export class StripeBillingService implements BillingService {
|
||||
}
|
||||
|
||||
async handleSubscriptionCreation(subscriptionId: string) {
|
||||
throw new Error("Method not implemented.");
|
||||
throw new Error(`Method not implemented for subscription id ${subscriptionId}`);
|
||||
}
|
||||
|
||||
async handleSubscriptionCancel(subscriptionId: string) {
|
||||
@@ -196,4 +200,45 @@ export class StripeBillingService implements BillingService {
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -42,7 +42,7 @@ vi.mock("@calcom/prisma", () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/features/ee/billing/stripe-billling-service", () => ({
|
||||
vi.mock("@calcom/features/ee/billing/stripe-billing-service", () => ({
|
||||
StripeBillingService: vi.fn().mockImplementation(() => ({
|
||||
createCustomer: vi.fn().mockResolvedValue({ id: "mock_customer_id" }),
|
||||
createPrice: vi.fn().mockResolvedValue({ id: "mock_price_id", isCustom: false }),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import {
|
||||
ORGANIZATION_SELF_SERVE_MIN_SEATS,
|
||||
ORGANIZATION_SELF_SERVE_PRICE,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OrganizationBilling" ADD COLUMN "subscriptionEnd" TIMESTAMP(3),
|
||||
ADD COLUMN "subscriptionStart" TIMESTAMP(3),
|
||||
ADD COLUMN "subscriptionTrialEnd" TIMESTAMP(3);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "TeamBilling" ADD COLUMN "subscriptionEnd" TIMESTAMP(3),
|
||||
ADD COLUMN "subscriptionStart" TIMESTAMP(3),
|
||||
ADD COLUMN "subscriptionTrialEnd" TIMESTAMP(3);
|
||||
@@ -2670,11 +2670,14 @@ model TeamBilling {
|
||||
teamId Int @unique
|
||||
team Team? @relation("TeamBilling", fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
subscriptionId String @unique
|
||||
subscriptionItemId String
|
||||
customerId String
|
||||
status String
|
||||
planName String
|
||||
subscriptionId String @unique
|
||||
subscriptionItemId String
|
||||
customerId String
|
||||
status String
|
||||
planName String
|
||||
subscriptionStart DateTime?
|
||||
subscriptionTrialEnd DateTime?
|
||||
subscriptionEnd DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -2685,11 +2688,14 @@ model OrganizationBilling {
|
||||
teamId Int @unique
|
||||
team Team @relation("OrganizationBilling", fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
subscriptionId String @unique
|
||||
subscriptionItemId String
|
||||
customerId String
|
||||
status String
|
||||
planName String
|
||||
subscriptionId String @unique
|
||||
subscriptionItemId String
|
||||
customerId String
|
||||
status String
|
||||
planName String
|
||||
subscriptionStart DateTime?
|
||||
subscriptionTrialEnd DateTime?
|
||||
subscriptionEnd DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||
|
||||
@@ -4,7 +4,8 @@ import type { GetServerSidePropsContext, NextApiResponse } from "next";
|
||||
|
||||
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { sendChangeOfEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -12,7 +13,6 @@ import logger from "@calcom/lib/logger";
|
||||
import { uploadAvatar } from "@calcom/lib/server/avatar";
|
||||
import { checkUsername } from "@calcom/features/profile/lib/checkUsername";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries";
|
||||
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||
|
||||
Reference in New Issue
Block a user