;
diff --git a/types/user.ts b/types/user.ts
index bed328804c..5aa4693606 100644
--- a/types/user.ts
+++ b/types/user.ts
@@ -1,14 +1,19 @@
import type { ExportFormatV4 } from './export';
+import type { SubscriptionPlan } from './paid_plan';
import type { PluginID } from './plugin';
+import type {
+ PaidPlanCurrencyType,
+ StripeProductPaidPlanType,
+} from './stripe-product';
import type { SupabaseClient } from '@supabase/supabase-js';
+export type { SubscriptionPlan } from './paid_plan';
+
export interface User extends UserProfile {
email: string;
}
-export type SubscriptionPlan = 'free' | 'pro' | 'ultra' | 'edu';
-
export interface UserConversation {
id: string;
uid: string;
@@ -56,3 +61,8 @@ export type UserProfileQuery = RequiredOne<
UserProfileQueryProps,
'userId' | 'email'
>;
+
+export interface UserSubscriptionDetail {
+ userPlan: StripeProductPaidPlanType | null;
+ subscriptionCurrency: PaidPlanCurrencyType;
+}
diff --git a/utils/app/const.ts b/utils/app/const.ts
index d39739259f..64a07d62e7 100644
--- a/utils/app/const.ts
+++ b/utils/app/const.ts
@@ -1,8 +1,18 @@
import { OpenAIModels, fallbackModelID } from '@/types/openai';
+import type { SubscriptionPlan } from '@/types/user';
import dayjs from 'dayjs';
import { v4 as uuidv4 } from 'uuid';
+export {
+ STRIPE_PAID_PLAN_LINKS,
+ STRIPE_PLAN_CODE_GPT4_CREDIT,
+ STRIPE_PLAN_CODE_IMAGE_CREDIT,
+ GPT4_CREDIT_PURCHASE_LINKS,
+ AI_IMAGE_CREDIT_PURCHASE_LINKS,
+ V2_CHAT_UPGRADE_LINK,
+} from './stripe/stripe_config';
+
export const RESPONSE_IN_CHINESE_PROMPT = `Whenever you respond in Chinese, you must respond in Traditional Chinese (繁體中文).`;
export const DEFAULT_SYSTEM_PROMPT =
@@ -129,6 +139,13 @@ export const newDefaultConversation = {
lastUpdateAtUTC: dayjs().valueOf(),
};
+export const OrderedSubscriptionPlans: SubscriptionPlan[] = [
+ 'free',
+ 'pro',
+ 'ultra',
+ 'edu',
+];
+
// Gemini File Upload Constants
// NOTE: https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models
// NOTE:https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/document-understanding
diff --git a/utils/app/eventTracking.ts b/utils/app/eventTracking.ts
index 6366cb123e..e550d250c6 100644
--- a/utils/app/eventTracking.ts
+++ b/utils/app/eventTracking.ts
@@ -162,7 +162,6 @@ export type PayloadType = {
mjQueueCleanupJobOneWeekAgo?: string;
mjQueueCleanupJobFiveMinutesAgo?: string;
mjQueueJobDetail?: MjJob;
-
};
export interface UserPostHogProfile {
diff --git a/utils/app/stripe/stripe_config.ts b/utils/app/stripe/stripe_config.ts
new file mode 100644
index 0000000000..5e32916d8d
--- /dev/null
+++ b/utils/app/stripe/stripe_config.ts
@@ -0,0 +1,54 @@
+import {
+ STRIPE_PAID_PLAN_LINKS_PRODUCTION,
+ STRIPE_PAID_PLAN_LINKS_STAGING,
+} from './stripe_paid_plan_links_config';
+import {
+ STRIPE_PRODUCT_LIST_PRODUCTION,
+ STRIPE_PRODUCT_LIST_STAGING,
+} from './stripe_product_list_config';
+
+// STRIPE CREDIT CODE
+export const STRIPE_PLAN_CODE_GPT4_CREDIT = 'GPT4_CREDIT';
+export const STRIPE_PLAN_CODE_IMAGE_CREDIT = 'IMAGE_CREDIT';
+
+export const STRIPE_PRODUCT_LIST =
+ process.env.NEXT_PUBLIC_ENV === 'production'
+ ? STRIPE_PRODUCT_LIST_PRODUCTION
+ : STRIPE_PRODUCT_LIST_STAGING;
+
+export const STRIPE_PAID_PLAN_LINKS =
+ process.env.NEXT_PUBLIC_ENV === 'production'
+ ? STRIPE_PAID_PLAN_LINKS_PRODUCTION
+ : STRIPE_PAID_PLAN_LINKS_STAGING;
+
+// =========== TOP UP LINKS ===========
+export const GPT4_CREDIT_PURCHASE_LINKS = {
+ '50':
+ process.env.NEXT_PUBLIC_ENV === 'production'
+ ? 'https://buy.stripe.com/28o03Z0vE3Glak09AJ'
+ : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b',
+ '150':
+ process.env.NEXT_PUBLIC_ENV === 'production'
+ ? 'https://buy.stripe.com/cN2dUP6U2dgV0JqcMW'
+ : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b',
+ '300':
+ process.env.NEXT_PUBLIC_ENV === 'production'
+ ? 'https://buy.stripe.com/dR6g2Xemu5Otcs83cn'
+ : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b',
+};
+export const AI_IMAGE_CREDIT_PURCHASE_LINKS = {
+ '100':
+ process.env.NEXT_PUBLIC_ENV === 'production'
+ ? 'https://buy.stripe.com/fZeg2Xdiq4Kp8bS9AT'
+ : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b',
+ '500':
+ process.env.NEXT_PUBLIC_ENV === 'production'
+ ? 'https://buy.stripe.com/8wMg2XcemccR2Ry8wQ'
+ : 'https://buy.stripe.com/test_9AQ01v8fabrccp228b',
+};
+
+// =========== V2 UPGRADE LINKS ===========
+export const V2_CHAT_UPGRADE_LINK =
+ process.env.NEXT_PUBLIC_ENV === 'production'
+ ? 'https://buy.stripe.com/4gw9Ez6U2gt71NudRd'
+ : 'https://buy.stripe.com/test_dR68y152Y7aWagUcMU';
diff --git a/utils/app/stripe/stripe_paid_plan_links_config.ts b/utils/app/stripe/stripe_paid_plan_links_config.ts
new file mode 100644
index 0000000000..de6297f0cc
--- /dev/null
+++ b/utils/app/stripe/stripe_paid_plan_links_config.ts
@@ -0,0 +1,79 @@
+import type { PaidPlanLinks } from '@/types/stripe-product';
+
+export const STRIPE_PAID_PLAN_LINKS_PRODUCTION: PaidPlanLinks = {
+ 'ultra-yearly': {
+ twd: {
+ // $8,800.00 TWD / year
+ link: 'https://buy.stripe.com/6oEaID6U2b8Ncs85kK',
+ price_id: 'price_1PGVh6EEvfd1Bzvu3OyJGTZ2',
+ },
+ usd: {
+ // $279.99 USD / year
+ link: 'https://buy.stripe.com/fZebMH0vEdgVeAg3cF',
+ price_id: 'price_1PMSCyEEvfd1Bzvuk7VHjx6S',
+ },
+ },
+ 'ultra-monthly': {
+ twd: {
+ // $880.00 TWD / month
+ link: 'https://buy.stripe.com/8wMeYT92aekZ0Jq9B1',
+ price_id: 'price_1PMS9KEEvfd1BzvuBCA4LAJA',
+ },
+ usd: {
+ // $29.99 USD / month
+ link: 'https://buy.stripe.com/4gwbMH6U27WB9fW9B2',
+ price_id: 'price_1PMSBdEEvfd1BzvuqUuMvUv7',
+ },
+ },
+ 'pro-monthly': {
+ twd: {
+ // $249.99 TWD / month
+ link: 'https://buy.stripe.com/dR65oj2DM90FeAgcNg',
+ price_id: 'price_1PMSIDEEvfd1BzvuegdR9cyP',
+ },
+ usd: {
+ // $9.99 USD / month
+ link: 'https://buy.stripe.com/8wM8Av2DM0u99fWfZ1',
+ price_id: 'price_1N1VMjEEvfd1BzvuWqqVu9YZ',
+ },
+ },
+};
+
+export const STRIPE_PAID_PLAN_LINKS_STAGING: PaidPlanLinks = {
+ 'ultra-yearly': {
+ twd: {
+ // $8,800.00 TWD / year
+ link: 'https://buy.stripe.com/test_8wM9C5fHCan8agUdR9',
+ price_id: 'price_1PLiWVEEvfd1Bzvu7voi21Jw',
+ },
+ usd: {
+ // $279.99 USD / year
+ link: 'https://buy.stripe.com/test_3csaG952Y2UG74IfZg',
+ price_id: 'price_1PLiWmEEvfd1BzvuDFmiLKI6',
+ },
+ },
+ 'ultra-monthly': {
+ twd: {
+ // $880.00 TWD / month
+ link: 'https://buy.stripe.com/test_fZe6pT1QM1QC2Os6oF',
+ price_id: 'price_1PLiWBEEvfd1BzvunVr1yZ55',
+ },
+ usd: {
+ // $29.99 USD / month
+ link: 'https://buy.stripe.com/test_cN29C5dzu8f0dt6fZe',
+ price_id: 'price_1PLhlhEEvfd1Bzvu0UEqwm9y',
+ },
+ },
+ 'pro-monthly': {
+ twd: {
+ // $249.99 TWD / month
+ link: 'https://buy.stripe.com/test_6oE01v1QM66S74I7sH',
+ price_id: 'price_1PLhJREEvfd1BzvuxCM477DD',
+ },
+ usd: {
+ // $9.99 USD / month
+ link: 'https://buy.stripe.com/test_4gw4hLcvq52Odt6fYY',
+ price_id: 'price_1N09fTEEvfd1BzvuJwBCAfg2',
+ },
+ },
+};
diff --git a/utils/app/stripe/stripe_product_list_config.ts b/utils/app/stripe/stripe_product_list_config.ts
new file mode 100644
index 0000000000..6fc6987e78
--- /dev/null
+++ b/utils/app/stripe/stripe_product_list_config.ts
@@ -0,0 +1,123 @@
+import type { NewStripeProduct } from '@/types/stripe-product';
+
+export const STRIPE_PRODUCT_LIST_STAGING: NewStripeProduct[] = [
+ {
+ type: 'paid_plan',
+ mode: 'payment',
+ givenDays: 7,
+ productValue: 'pro',
+ productId: 'prod_P39kXR1vgNBVZh',
+ note: 'This is a v2 weekly Pro plan',
+ },
+ {
+ type: 'paid_plan',
+ mode: 'subscription',
+ productValue: 'ultra',
+ productId: 'prod_QC5xRJFNyaB3h7',
+ },
+ {
+ type: 'paid_plan',
+ mode: 'subscription',
+ productValue: 'pro',
+ productId: 'prod_Nlh3dRKPO799ja',
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '50_GPT4_CREDIT',
+ productId: 'prod_OKKu2YQZyaJTYN',
+ credit: 50,
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '150_GPT4_CREDIT',
+ productId: 'prod_OKKu2YQZyaJTYN',
+ credit: 150,
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '300_GPT4_CREDIT',
+ productId: 'prod_OKKu2YQZyaJTYN',
+ credit: 300,
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '500_IMAGE_CREDIT',
+ productId: 'prod_OKJgVwM66OOWuR',
+ credit: 500,
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '100_IMAGE_CREDIT',
+ productId: 'prod_OKJgVwM66OOWuR',
+ credit: 100,
+ },
+];
+export const STRIPE_PRODUCT_LIST_PRODUCTION: NewStripeProduct[] = [
+ {
+ type: 'paid_plan',
+ mode: 'payment',
+ givenDays: 7,
+ productValue: 'pro',
+ productId: 'prod_P39h2AVZANAN1M',
+ note: 'This is a v2 weekly Pro plan',
+ },
+ {
+ type: 'paid_plan',
+ mode: 'subscription',
+ productValue: 'ultra',
+ productId: 'prod_Q6j96oOouZMFN4',
+ },
+ {
+ type: 'paid_plan',
+ mode: 'subscription',
+ productValue: 'ultra',
+ productId: 'prod_PGES2QxP0aYFr4',
+ note: 'This is the pre-sell plan for the Ultra plan',
+ },
+ {
+ type: 'paid_plan',
+ mode: 'subscription',
+ productValue: 'pro',
+ productId: 'prod_NlR6az1csuoBHl',
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '50_GPT4_CREDIT',
+ productId: 'prod_Nofw6ncuYiYD81',
+ credit: 50,
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '150_GPT4_CREDIT',
+ productId: 'prod_Nog8i22B8eLX6y',
+ credit: 150,
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '300_GPT4_CREDIT',
+ productId: 'prod_Nog91rmXzSJY1w',
+ credit: 300,
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '100_IMAGE_CREDIT',
+ productId: 'prod_OKYWXZGkysAtFS',
+ credit: 100,
+ },
+ {
+ type: 'top_up',
+ mode: 'payment',
+ productValue: '500_IMAGE_CREDIT',
+ productId: 'prod_OKYouQss6inD9q',
+ credit: 500,
+ },
+];
diff --git a/utils/app/ui.tsx b/utils/app/ui.tsx
index 36897ee270..12a4cd69fb 100644
--- a/utils/app/ui.tsx
+++ b/utils/app/ui.tsx
@@ -41,7 +41,7 @@ export const PlanDetail = {
'LINE connection',
'Unlimited GPT-4',
'Unlimited MidJourney generation',
- 'Chat with document (coming soon)',
+ 'Chat with document',
],
},
combinedSimplify: [
diff --git a/utils/server/mjServiceServerHelper.ts b/utils/server/mjServiceServerHelper.ts
index 6c0c9ce6ac..bd76af7844 100644
--- a/utils/server/mjServiceServerHelper.ts
+++ b/utils/server/mjServiceServerHelper.ts
@@ -35,7 +35,7 @@ export const trackFailedEvent = (jobInfo: MjJob, errorMessage: string) => {
mjImageGenTotalProcessingTimeInSeconds: totalProcessingTimeInSeconds,
mjImageGenErrorMessage: errorMessage,
usedOnDemandCredit: jobInfo.usedOnDemandCredit || false,
- lastUsedKey: jobInfo.lastUsedKey
+ lastUsedKey: jobInfo.lastUsedKey,
},
);
return trackEventPromise;
@@ -67,7 +67,7 @@ export const trackSuccessEvent = (jobInfo: MjJob) => {
totalWaitingInQueueTimeInSeconds,
mjImageGenTotalProcessingTimeInSeconds: totalProcessingTimeInSeconds,
usedOnDemandCredit: jobInfo.usedOnDemandCredit || false,
- lastUsedKey: jobInfo.lastUsedKey
+ lastUsedKey: jobInfo.lastUsedKey,
},
);
@@ -82,8 +82,8 @@ export const trackCleanupJobEvent = ({
fiveMinutesAgo,
}: {
event:
- | 'MJ Queue Cleanup Completed / Failed Job'
- | 'MJ Queue Cleanup Processing Job';
+ | 'MJ Queue Cleanup Completed / Failed Job'
+ | 'MJ Queue Cleanup Processing Job';
executedAt: string;
enqueuedAt: string;
oneWeekAgo?: string;
@@ -120,8 +120,9 @@ export const OriginalMjLogEvent = async ({
if (errorMessage) {
payloadToLog.imageGenerationFailed = 'true';
payloadToLog.imageGenerationErrorMessage = errorMessage;
- payloadToLog.imageGenerationPrompt = `${promptBeforeProcessing || 'N/A'
- } -> ${generationPrompt || 'N/A'}`;
+ payloadToLog.imageGenerationPrompt = `${
+ promptBeforeProcessing || 'N/A'
+ } -> ${generationPrompt || 'N/A'}`;
}
await serverSideTrackEvent(userId, 'AI image generation', payloadToLog);
diff --git a/utils/server/resend.ts b/utils/server/resend.ts
index 9f767da000..512afadc0d 100644
--- a/utils/server/resend.ts
+++ b/utils/server/resend.ts
@@ -45,48 +45,55 @@ export async function sendReportForStripeWebhookError(
subject = '',
event: Stripe.Event,
user?: UserProfile,
+ cause?: any,
) {
const session = event.data.object as Stripe.Checkout.Session;
const stripeSubscriptionId = session.subscription as string;
- const paymentDateInCanadaTime = dayjs
- .tz(session.created * 1000, 'America/Toronto')
- .format('YYYY-MM-DD HH:mm:ss');
- const paymentDateInTaiwanTime = dayjs
- .tz(session.created * 1000, 'Asia/Taipei')
- .format('YYYY-MM-DD HH:mm:ss');
+
+ const formatDate = (timestamp: number, timezone: string) =>
+ dayjs.tz(timestamp * 1000, timezone).format('YYYY-MM-DD HH:mm:ss');
+
+ const paymentDateInCanadaTime = formatDate(
+ session.created,
+ 'America/Toronto',
+ );
+ const paymentDateInTaiwanTime = formatDate(session.created, 'Asia/Taipei');
+
const paymentDateHTML = `
- Stripe Session Date(payment date): ${paymentDateInCanadaTime} (Canada Time)
- Stripe Session Date(payment date): ${paymentDateInTaiwanTime} (Taiwan Time)
+ Stripe Session Date(payment date): ${paymentDateInCanadaTime} (Canada Time)
+ Stripe Session Date(payment date): ${paymentDateInTaiwanTime} (Taiwan Time)
`;
const userDataHTML = user
? `
- User id: ${user?.id}
- User Email: ${user?.email}
- `
+ User id: ${user.id}
+ User Email: ${user.email}
+ `
: '';
+
const referenceDataHTML = session
? `
- Stripe Session Reference
-
- ${JSON.stringify(session)}
-`
- : `
-Stripe Event
-
-${JSON.stringify(event)}
-
-`;
- await sendReport(
- `Stripe Webhook Error - ${subject}`,
+ Stripe Session Reference
+ ${JSON.stringify(session, null, 2)}
+ Cause data
+ ${JSON.stringify(cause, null, 2)}
`
+ : `
+ Stripe Event
+ ${JSON.stringify(event, null, 2)}
+ Cause data
+ ${JSON.stringify(cause, null, 2)}
+ `;
+
+ const emailHTML = `
Stripe Webhook Error - ${subject}
Stripe Session Id: ${session.id}
${paymentDateHTML}
- Stripe event type : ${event.type}
- Stripe Strip Subscription Id: ${stripeSubscriptionId}
+ Stripe event type: ${event.type}
+ Stripe Subscription Id: ${stripeSubscriptionId}
${userDataHTML}
${referenceDataHTML}
- `,
- );
+ `;
+
+ await sendReport(`Stripe Webhook Error - ${subject}`, emailHTML);
}
diff --git a/utils/server/stripe/getCustomerEmailByCustomerID.ts b/utils/server/stripe/getCustomerEmailByCustomerID.ts
deleted file mode 100644
index 609a3658d0..0000000000
--- a/utils/server/stripe/getCustomerEmailByCustomerID.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import Stripe from 'stripe';
-
-export default async function getCustomerEmailByCustomerID(
- customerID: string,
-): Promise {
- try {
- const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
- apiVersion: '2022-11-15',
- });
-
- // We get the customer id from webhook, so we know the customer is not deleted
- const customer = (await stripe.customers.retrieve(
- customerID,
- )) as Stripe.Customer;
- if (!customer.email) {
- throw new Error(
- `the customer does not have an email, customer id is ${customerID}`,
- );
- }
- return customer.email;
- } catch (e) {
- throw new Error(`getCustomerEmailByCustomerID failed: ${e}`);
- }
-}
diff --git a/utils/server/stripe/handleCheckoutSessionCompleted.ts b/utils/server/stripe/handleCheckoutSessionCompleted.ts
index 732850d22b..7b797465dd 100644
--- a/utils/server/stripe/handleCheckoutSessionCompleted.ts
+++ b/utils/server/stripe/handleCheckoutSessionCompleted.ts
@@ -1,145 +1,105 @@
import { serverSideTrackEvent } from '@/utils/app/eventTracking';
import { PluginID } from '@/types/plugin';
+import type {
+ NewStripeProduct,
+ StripeOneTimePaidPlanProduct,
+} from '@/types/stripe-product';
+import type { UserProfile } from '@/types/user';
import {
addCredit,
getAdminSupabaseClient,
userProfileQuery,
} from '../supabase';
-import updateUserAccount from './updateUserAccount';
+import StripeHelper, {
+ updateUserAccountByEmail,
+ updateUserAccountById,
+} from './strip_helper';
import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc';
import type Stripe from 'stripe';
-const ONE_TIME_PRO_PLAN_FOR_1_MONTH =
- process.env.STRIPE_PLAN_CODE_ONE_TIME_PRO_PLAN_FOR_1_MONTH;
+const supabase = getAdminSupabaseClient();
-const IMAGE_CREDIT = process.env.STRIPE_PLAN_CODE_IMAGE_CREDIT;
-const GPT4_CREDIT = process.env.STRIPE_PLAN_CODE_GPT4_CREDIT;
+dayjs.extend(utc);
export default async function handleCheckoutSessionCompleted(
session: Stripe.Checkout.Session,
+ isFakeEvent = false,
): Promise {
const userId = session.client_reference_id;
const email = session.customer_details?.email;
- const planCode = session.metadata?.plan_code;
- const planGivingWeeks = session.metadata?.plan_giving_weeks;
- const credit = session.metadata?.credit;
const stripeSubscriptionId = session.subscription as string;
- if (!planCode && !planGivingWeeks) {
- throw new Error('no plan code or plan giving weeks from Stripe webhook');
- }
+ const sessionId = session.id;
+ const product = await StripeHelper.product.getProductByCheckoutSessionId(
+ sessionId,
+ session.mode,
+ );
if (!email) {
throw new Error('missing Email from Stripe webhook');
}
-
- const isTopUpCreditRequest =
- (planCode === IMAGE_CREDIT || planCode === GPT4_CREDIT) && credit;
- // Handle TopUp Image Credit / GPT4 Credit
- if (isTopUpCreditRequest) {
- return await addCreditToUser(
- email,
- +credit,
- planCode === IMAGE_CREDIT ? PluginID.IMAGE_GEN : PluginID.GPT4,
- );
- }
-
- const sinceDate = dayjs.unix(session.created).utc().toDate();
- const proPlanExpirationDate = await getProPlanExpirationDate(
- planGivingWeeks,
- planCode,
+ const user = await userProfileQuery({
+ client: supabase,
email,
- sinceDate,
- );
-
- serverSideTrackEvent(userId || 'N/A', 'New paying customer', {
- paymentDetail:
- !session.amount_subtotal || session.amount_subtotal <= 50000
- ? 'One-time'
- : 'Monthly',
});
- // Update user account by User id
- if (userId) {
- await updateUserAccount({
- upgrade: true,
- userId,
- stripeSubscriptionId,
- proPlanExpirationDate: proPlanExpirationDate,
- });
- } else {
- // Update user account by Email
- await updateUserAccount({
- upgrade: true,
- email: email!,
- stripeSubscriptionId,
- proPlanExpirationDate: proPlanExpirationDate,
- });
- }
-}
-
-async function getProPlanExpirationDate(
- planGivingWeeks: string | undefined,
- planCode: string | undefined,
- email: string,
- sinceDate: Date,
-) {
- // Takes plan_giving_weeks priority over plan_code
- if (planGivingWeeks && typeof planGivingWeeks === 'string') {
- // Get users' pro expiration date
- const supabase = getAdminSupabaseClient();
- const user = await userProfileQuery({
- client: supabase,
- email,
- });
- const userProPlanExpirationDate = user?.proPlanExpirationDate;
- if (userProPlanExpirationDate) {
- // when user bought one-time pro plan previously or user has referral trial
- return dayjs(userProPlanExpirationDate)
- .add(+planGivingWeeks, 'week')
- .toDate();
- } else if (user.plan === 'pro' && !user.proPlanExpirationDate) {
- // when user is pro monthly subscriber
- throw new Error(
- 'Monthly Pro subscriber bought one-time pro plan, should not happen',
- {
- cause: {
- user,
- },
- },
+ // # Upgrade plan flow
+ if (product.type === 'paid_plan') {
+ if (session.mode === 'subscription') {
+ // Recurring payment flow
+ await handleSubscription(
+ session,
+ user,
+ product,
+ stripeSubscriptionId,
+ userId || undefined,
+ email || undefined,
+ isFakeEvent,
+ );
+ } else if (session.mode === 'payment') {
+ // One-time payment flow
+ await handleOneTimePayment(
+ session,
+ user,
+ product as StripeOneTimePaidPlanProduct,
+ stripeSubscriptionId,
+ userId || undefined,
+ email || undefined,
);
} else {
- // when user is not pro yet
- return dayjs(sinceDate).add(+planGivingWeeks, 'week').toDate();
+ throw new Error(`Unhandled session mode ${session.mode}`, {
+ cause: {
+ session,
+ product,
+ },
+ });
}
- } else if (
- planCode?.toUpperCase() === ONE_TIME_PRO_PLAN_FOR_1_MONTH?.toUpperCase()
- ) {
- // Only store expiration for one month plan
- return dayjs(sinceDate).add(1, 'month').toDate();
- } else {
- return undefined;
+ } else if (product.type === 'top_up') {
+ // Top Up Credit flow
+ return await addCreditToUser(
+ user,
+ product.credit,
+ product.productValue === '500_IMAGE_CREDIT' ||
+ product.productValue === '100_IMAGE_CREDIT'
+ ? PluginID.IMAGE_GEN
+ : PluginID.GPT4,
+ );
}
}
async function addCreditToUser(
- email: string,
+ user: UserProfile,
credit: number,
creditType: Exclude<
PluginID,
PluginID.LANGCHAIN_CHAT | PluginID.IMAGE_TO_PROMPT
>,
) {
- // Get user id by email address
- const supabase = getAdminSupabaseClient();
- const user = await userProfileQuery({
- client: supabase,
- email,
- });
// Check is Pro user
if (user.plan === 'free') {
throw Error(`A free user try to top up ${creditType}}`, {
@@ -167,3 +127,121 @@ async function addCreditToUser(
credit,
);
}
+
+async function handleSubscription(
+ session: Stripe.Checkout.Session,
+ user: UserProfile,
+ product: NewStripeProduct,
+ stripeSubscriptionId: string,
+ userId: string | undefined,
+ email: string | undefined,
+ isFakeEvent = false,
+) {
+ const subscription =
+ await StripeHelper.subscription.getSubscriptionById(stripeSubscriptionId);
+ // NOTE: Since the stripeSubscriptionId is fake, we use the current date to simulate the expiration date
+ const currentPeriodEnd = dayjs
+ .unix(
+ isFakeEvent
+ ? dayjs().add(1, 'month').unix()
+ : subscription.current_period_end,
+ )
+ .utc()
+ .toDate();
+
+ const userIsInPaidPlan = user.plan !== 'free' && user.plan !== 'edu';
+ if (userIsInPaidPlan) {
+ throw new Error(
+ 'User is already in a paid plan, cannot purchase a new plan, should issue an refund',
+ {
+ cause: {
+ user,
+ buyingProduct: product,
+ session,
+ },
+ },
+ );
+ }
+ const subscriptionBillPeriod = (() => {
+ switch (subscription.items.data?.[0].plan?.interval) {
+ case 'month':
+ return 'Monthly';
+ case 'year':
+ return 'Yearly';
+ case 'week':
+ return 'Weekly';
+ case 'day':
+ return 'Daily';
+ default:
+ return 'Monthly';
+ }
+ })();
+ serverSideTrackEvent(userId || 'N/A', 'New paying customer', {
+ paymentDetail: subscriptionBillPeriod,
+ });
+
+ // Update user account by User id
+ if (userId) {
+ await updateUserAccountById({
+ userId,
+ plan: product.productValue,
+ stripeSubscriptionId,
+ proPlanExpirationDate: currentPeriodEnd,
+ });
+ } else {
+ // Update user account by Email
+ await updateUserAccountByEmail({
+ email: email!,
+ plan: product.productValue,
+ stripeSubscriptionId,
+ proPlanExpirationDate: currentPeriodEnd,
+ });
+ }
+}
+
+async function handleOneTimePayment(
+ session: Stripe.Checkout.Session,
+ user: UserProfile,
+ product: StripeOneTimePaidPlanProduct,
+ stripeSubscriptionId: string,
+ userId: string | undefined,
+ email: string | undefined,
+) {
+ const productGivenDays = product.givenDays;
+ const currentPeriodEnd = dayjs().add(productGivenDays, 'day').utc().toDate();
+
+ const userIsInPaidPlan = user.plan !== 'free' && user.plan !== 'edu';
+ if (userIsInPaidPlan) {
+ throw new Error(
+ 'User is already in a paid plan, cannot purchase a new plan, should issue an refund',
+ {
+ cause: {
+ user,
+ buyingProduct: product,
+ session,
+ },
+ },
+ );
+ }
+ serverSideTrackEvent(userId || 'N/A', 'New paying customer', {
+ paymentDetail: 'One-time',
+ });
+
+ // Update user account by User id
+ if (userId) {
+ await updateUserAccountById({
+ userId,
+ plan: product.productValue,
+ stripeSubscriptionId,
+ proPlanExpirationDate: currentPeriodEnd,
+ });
+ } else {
+ // Update user account by Email
+ await updateUserAccountByEmail({
+ email: email!,
+ plan: product.productValue,
+ stripeSubscriptionId,
+ proPlanExpirationDate: currentPeriodEnd,
+ });
+ }
+}
diff --git a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts
index 1030bd7e50..c977e66fbc 100644
--- a/utils/server/stripe/handleCustomerSubscriptionDeleted.ts
+++ b/utils/server/stripe/handleCustomerSubscriptionDeleted.ts
@@ -1,24 +1,59 @@
-import getCustomerEmailByCustomerID from './getCustomerEmailByCustomerID';
-import updateUserAccount from './updateUserAccount';
+import { getAdminSupabaseClient } from '../supabase';
+import { getCustomerEmailByCustomerID } from './strip_helper';
+import { TEST_PAYMENT_USER } from '@/cypress/e2e/account';
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc';
import type Stripe from 'stripe';
+const supabase = getAdminSupabaseClient();
+dayjs.extend(utc);
+
export default async function handleCustomerSubscriptionDeleted(
session: Stripe.Subscription,
+ isFakeEvent = false,
): Promise {
+ // Add 1 day to the current date to avoid time zone differences
+
+ if (isFakeEvent) {
+ const { error: updatedUserError } = await supabase
+ .from('profiles')
+ .update({
+ pro_plan_expiration_date: dayjs()
+ .add(1, 'month')
+ .add(1, 'day')
+ .utc()
+ .toDate(),
+ })
+ .eq('email', TEST_PAYMENT_USER.email);
+ if (updatedUserError) throw updatedUserError;
+ return;
+ }
+
+ const newExpirationDate = dayjs
+ .unix(session.current_period_end)
+ .add(1, 'day')
+ .utc()
+ .toDate();
const stripeSubscriptionId = session.id;
if (!stripeSubscriptionId) {
const customerId = session.customer as string;
const email = await getCustomerEmailByCustomerID(customerId);
- await updateUserAccount({
- upgrade: false,
- email,
- });
+ const { error: updatedUserError } = await supabase
+ .from('profiles')
+ .update({
+ pro_plan_expiration_date: newExpirationDate,
+ })
+ .eq('email', email);
+ if (updatedUserError) throw updatedUserError;
} else {
- await updateUserAccount({
- upgrade: false,
- stripeSubscriptionId,
- });
+ const { error: updatedUserError } = await supabase
+ .from('profiles')
+ .update({
+ pro_plan_expiration_date: newExpirationDate,
+ })
+ .eq('stripe_subscription_id', stripeSubscriptionId);
+ if (updatedUserError) throw updatedUserError;
}
}
diff --git a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts
index 2d58708807..d433095d2e 100644
--- a/utils/server/stripe/handleCustomerSubscriptionUpdated.ts
+++ b/utils/server/stripe/handleCustomerSubscriptionUpdated.ts
@@ -1,10 +1,14 @@
-import getCustomerEmailByCustomerID from './getCustomerEmailByCustomerID';
-import updateUserAccount from './updateUserAccount';
+import { getAdminSupabaseClient } from '../supabase';
+import StripeHelper, {
+ downgradeUserAccount,
+ getCustomerEmailByCustomerID,
+} from './strip_helper';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import type Stripe from 'stripe';
+const supabase = getAdminSupabaseClient();
dayjs.extend(utc);
export default async function handleCustomerSubscriptionUpdated(
@@ -12,43 +16,65 @@ export default async function handleCustomerSubscriptionUpdated(
): Promise {
const stripeSubscriptionId = session.id;
- if (!session.cancel_at) {
- return;
- }
+ const currentPeriodStart = dayjs
+ .unix(session.current_period_start)
+ .utc()
+ .toDate();
+ const currentPeriodEnd = dayjs
+ .unix(session.current_period_end)
+ .utc()
+ .toDate();
- const cancelAtDate = dayjs.unix(session.cancel_at!).utc().toDate();
+ console.log({
+ currentPeriodStart,
+ currentPeriodEnd,
+ });
+ const cancelAtDate = session.cancel_at
+ ? dayjs.unix(session.cancel_at).utc().toDate()
+ : null;
const today = dayjs().utc().toDate();
- if (cancelAtDate < today) {
+ if (cancelAtDate && cancelAtDate < today) {
// Downgrade to free plan
if (!stripeSubscriptionId) {
const customerId = session.customer as string;
const email = await getCustomerEmailByCustomerID(customerId);
- await updateUserAccount({
- upgrade: false,
+ await downgradeUserAccount({
email,
});
} else {
- await updateUserAccount({
- upgrade: false,
+ await downgradeUserAccount({
stripeSubscriptionId,
});
}
} else {
- // Monthly Pro Plan Subscription recurring payment, extend expiration date
+ // Monthly Pro / Ultra Plan Subscription recurring payment, extend expiration date / change to new plan
if (!stripeSubscriptionId) {
- const customerId = session.customer as string;
- const email = await getCustomerEmailByCustomerID(customerId);
- await updateUserAccount({
- upgrade: true,
- email,
- });
- } else {
- await updateUserAccount({
- upgrade: true,
- stripeSubscriptionId,
- proPlanExpirationDate: undefined,
+ throw new Error('Stripe subscription ID not found');
+ }
+
+ const userSubscription =
+ await StripeHelper.subscription.getSubscriptionById(stripeSubscriptionId);
+ const productId = userSubscription.items.data[0].price.product;
+ if (!productId || typeof productId !== 'string') {
+ throw new Error('The session does not have a product id', {
+ cause: {
+ session,
+ },
});
}
+ const product = await StripeHelper.product.getProductByProductId(
+ productId,
+ 'subscription',
+ );
+ if (product.type !== 'paid_plan') return;
+ const { error: updatedUserError } = await supabase
+ .from('profiles')
+ .update({
+ plan: product.productValue,
+ pro_plan_expiration_date: currentPeriodEnd,
+ })
+ .eq('stripe_subscription_id', stripeSubscriptionId);
+ if (updatedUserError) throw updatedUserError;
}
}
diff --git a/utils/server/stripe/strip_helper.ts b/utils/server/stripe/strip_helper.ts
new file mode 100644
index 0000000000..f90089ff6a
--- /dev/null
+++ b/utils/server/stripe/strip_helper.ts
@@ -0,0 +1,236 @@
+import { STRIPE_PRODUCT_LIST } from '@/utils/app/stripe/stripe_config';
+
+import { PaidPlan } from '@/types/paid_plan';
+
+import { getAdminSupabaseClient } from '../supabase';
+
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc';
+import Stripe from 'stripe';
+
+dayjs.extend(utc);
+
+const supabase = getAdminSupabaseClient();
+
+const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
+ apiVersion: '2022-11-15',
+});
+
+async function getSubscriptionById(subscriptionId: string) {
+ try {
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId);
+ return subscription;
+ } catch (error) {
+ console.error('Error retrieving subscription:', error);
+ throw error;
+ }
+}
+const StripeHelper = {
+ subscription: {
+ getSubscriptionById,
+ },
+ product: {
+ getProductByCheckoutSessionId: getProductByCheckoutSessionId,
+ getProductByProductId: getProductByProductId,
+ },
+};
+export default StripeHelper;
+
+async function getProductByCheckoutSessionId(
+ sessionId: string,
+ mode: Stripe.Checkout.Session.Mode,
+) {
+ const productId = await getProductIdByCheckoutSessionId(sessionId);
+ return getProductByProductId(productId, mode);
+}
+async function getProductByProductId(
+ productId: string,
+ mode: Stripe.Checkout.Session.Mode,
+) {
+ const product = STRIPE_PRODUCT_LIST.find(
+ (product) => product.productId === productId && product.mode === mode,
+ );
+ if (!product) {
+ throw new Error('No product found in our codebase', {
+ cause: {
+ productId,
+ mode,
+ STRIPE_PRODUCT_LIST,
+ },
+ });
+ }
+ return product;
+}
+
+async function getProductIdByCheckoutSessionId(
+ sessionId: string,
+): Promise {
+ const session = await stripe.checkout.sessions.retrieve(sessionId, {
+ expand: ['line_items'],
+ });
+ const lineItems = session.line_items;
+
+ const productId = lineItems?.data[0]?.price?.product;
+ if (!productId || typeof productId !== 'string') {
+ throw new Error('The session does not have a product id', {
+ cause: {
+ session,
+ },
+ });
+ }
+ return productId;
+}
+
+export async function fetchSubscriptionIdByUserId(
+ userId: string,
+): Promise {
+ const { data: userProfile } = await supabase
+ .from('profiles')
+ .select('stripe_subscription_id')
+ .eq('id', userId)
+ .single();
+ return userProfile?.stripe_subscription_id;
+}
+
+export async function getCustomerEmailByCustomerID(
+ customerID: string,
+): Promise {
+ try {
+ // We get the customer id from webhook, so we know the customer is not deleted
+ const customer = (await stripe.customers.retrieve(
+ customerID,
+ )) as Stripe.Customer;
+ if (!customer.email) {
+ throw new Error(
+ `the customer does not have an email, customer id is ${customerID}`,
+ );
+ }
+ return customer.email;
+ } catch (e) {
+ throw new Error(`getCustomerEmailByCustomerID failed: ${e}`);
+ }
+}
+
+export async function updateUserAccountById({
+ userId,
+ plan,
+ stripeSubscriptionId,
+ proPlanExpirationDate,
+}: {
+ userId: string;
+ plan?: string;
+ stripeSubscriptionId?: string;
+ proPlanExpirationDate?: Date;
+}) {
+ // Update user account by User ID
+ const { data: userProfile } = await supabase
+ .from('profiles')
+ .select('plan')
+ .eq('id', userId)
+ .single();
+
+ if (userProfile?.plan === 'edu') return;
+
+ const { error: updatedUserError } = await supabase
+ .from('profiles')
+ .update({
+ plan,
+ stripe_subscription_id: stripeSubscriptionId,
+ pro_plan_expiration_date: proPlanExpirationDate,
+ })
+ .eq('id', userId);
+ if (updatedUserError) throw updatedUserError;
+ console.log(`User ${userId} updated to ${plan}`);
+}
+
+export async function updateUserAccountByEmail({
+ email,
+ plan,
+ stripeSubscriptionId,
+ proPlanExpirationDate,
+}: {
+ email: string;
+ plan?: string;
+ stripeSubscriptionId?: string;
+ proPlanExpirationDate?: Date;
+}) {
+ const { data: userProfile } = await supabase
+ .from('profiles')
+ .select('plan')
+ .eq('email', email)
+ .single();
+
+ if (userProfile?.plan === 'edu') return;
+
+ const { error: updatedUserError } = await supabase
+ .from('profiles')
+ .update({
+ plan,
+ stripe_subscription_id: stripeSubscriptionId,
+ pro_plan_expiration_date: proPlanExpirationDate,
+ })
+ .eq('email', email);
+ if (updatedUserError) throw updatedUserError;
+ console.log(`User ${email} updated to ${plan}`);
+}
+
+export async function downgradeUserAccount({
+ email,
+ stripeSubscriptionId,
+}: {
+ email?: string;
+ stripeSubscriptionId?: string;
+}) {
+ if (!email && !stripeSubscriptionId) {
+ throw new Error('Either email or stripeSubscriptionId must be provided');
+ }
+
+ const { data: userProfile } = await supabase
+ .from('profiles')
+ .select('plan')
+ .eq(
+ stripeSubscriptionId ? 'stripe_subscription_id' : 'email',
+ stripeSubscriptionId || email,
+ )
+ .single();
+
+ if (userProfile?.plan === 'edu') return;
+
+ const { error: updatedUserError } = await supabase
+ .from('profiles')
+ .update({
+ plan: 'free',
+ })
+ .eq(
+ stripeSubscriptionId ? 'stripe_subscription_id' : 'email',
+ stripeSubscriptionId || email,
+ );
+ if (updatedUserError) throw updatedUserError;
+ console.log(`User ${email || stripeSubscriptionId} downgraded to free plan`);
+}
+
+export async function calculateMembershipExpirationDate(
+ planGivingWeeks: string | undefined,
+ planCode: string | undefined,
+ sessionCreatedDate: Date,
+): Promise {
+ const previousDate = dayjs(sessionCreatedDate || undefined);
+ // If has planGivingWeeks, use it to calculate the expiration date
+ if (planGivingWeeks && typeof planGivingWeeks === 'string') {
+ return previousDate.add(+planGivingWeeks, 'week').toDate();
+ }
+ // else extend the expiration date based on the plan code
+ else if (planCode === PaidPlan.ProOneTime) {
+ return previousDate.add(1, 'month').toDate();
+ } else if (planCode === PaidPlan.ProMonthly) {
+ return previousDate.add(1, 'month').toDate();
+ } else if (planCode === PaidPlan.UltraOneTime) {
+ return previousDate.add(1, 'month').toDate();
+ } else if (planCode === PaidPlan.UltraMonthly) {
+ return previousDate.add(1, 'month').toDate();
+ } else if (planCode === PaidPlan.UltraYearly) {
+ return previousDate.add(1, 'year').toDate();
+ }
+ // Return undefined if no conditions are met
+ return undefined;
+}
diff --git a/utils/server/stripe/updateUserAccount.ts b/utils/server/stripe/updateUserAccount.ts
deleted file mode 100644
index edb003ac2e..0000000000
--- a/utils/server/stripe/updateUserAccount.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-import { getAdminSupabaseClient } from '../supabase';
-
-// Skip any account operation on Edu accounts
-export default async function updateUserAccount(props: UpdateUserAccountProps) {
- const supabase = getAdminSupabaseClient();
-
- if (isUpgradeUserAccountProps(props)) {
- // Update user account
- const { data: userProfile } = await supabase
- .from('profiles')
- .select('plan')
- .eq('id', props.userId)
- .single();
-
- if (userProfile?.plan === 'edu') return;
-
- const { error: updatedUserError } = await supabase
- .from('profiles')
- .update({
- plan: 'pro',
- stripe_subscription_id: props.stripeSubscriptionId,
- pro_plan_expiration_date: props.proPlanExpirationDate || null,
- })
- .eq('id', props.userId);
-
- if (updatedUserError) throw updatedUserError;
- console.log(`User ${props.userId} updated to pro`);
- } else if (isUpgradeUserAccountByEmailProps(props)) {
- // Update user account by Email
- const { data: userProfile } = await supabase
- .from('profiles')
- .select('plan')
- .eq('email', props.email)
- .single();
-
- if (userProfile?.plan === 'edu') return;
-
- const { error: updatedUserError } = await supabase
- .from('profiles')
- .update({
- plan: 'pro',
- stripe_subscription_id: props.stripeSubscriptionId,
- pro_plan_expiration_date: props.proPlanExpirationDate || null,
- })
- .eq('email', props.email);
- if (updatedUserError) throw updatedUserError;
- console.log(`User ${props.email} updated to pro`);
- } else if (isDowngradeUserAccountProps(props)) {
- // Downgrade user account
-
- const { data: userProfile } = await supabase
- .from('profiles')
- .select('plan')
- .eq('stripe_subscription_id', props.stripeSubscriptionId)
- .single();
-
- if (userProfile?.plan === 'edu') return;
-
- const { error: updatedUserError } = await supabase
- .from('profiles')
- .update({
- plan: 'free',
- })
- .eq('stripe_subscription_id', props.stripeSubscriptionId);
- if (updatedUserError) throw updatedUserError;
- console.log(
- `User subscription ${props.stripeSubscriptionId} downgrade back to free`,
- );
- } else if (isDowngradeUserAccountByEmailProps(props)) {
- // Downgrade user account
- const { data: userProfile } = await supabase
- .from('profiles')
- .select('plan')
- .eq('email', props.email)
- .single();
-
- if (userProfile?.plan === 'edu') return;
-
- const { error: updatedUserError } = await supabase
- .from('profiles')
- .update({
- plan: 'free',
- })
- .eq('email', props.email);
- if (updatedUserError) throw updatedUserError;
- console.log(`User subscription ${props.email} downgrade back to free`);
- } else if (isExtendProPlanProps(props)) {
- // Extend pro plan
-
- const { data: userProfile } = await supabase
- .from('profiles')
- .select('plan')
- .eq('stripe_subscription_id', props.stripeSubscriptionId)
- .single();
-
- if (userProfile?.plan === 'edu') return;
-
- const { error: updatedUserError } = await supabase
- .from('profiles')
- .update({
- pro_plan_expiration_date: props.proPlanExpirationDate,
- })
- .eq('stripe_subscription_id', props.stripeSubscriptionId);
- if (updatedUserError) throw updatedUserError;
- console.log(
- `User subscription ${props.stripeSubscriptionId} extended to ${props.proPlanExpirationDate}`,
- );
- } else {
- throw new Error('Invalid props object');
- }
-}
-
-interface UpgradeUserAccountProps {
- upgrade: true;
- userId: string;
- stripeSubscriptionId?: string;
- proPlanExpirationDate?: Date;
-}
-
-interface UpgradeUserAccountByEmailProps {
- upgrade: true;
- email: string;
- stripeSubscriptionId?: string;
- proPlanExpirationDate?: Date;
-}
-
-interface DowngradeUserAccountProps {
- upgrade: false;
- stripeSubscriptionId: string;
- proPlanExpirationDate?: Date;
-}
-interface DowngradeUserAccountByEmailProps {
- upgrade: false;
- email: string;
- proPlanExpirationDate?: Date;
-}
-
-interface ExtendProPlanProps {
- upgrade: true;
- stripeSubscriptionId: string;
- proPlanExpirationDate: Date | undefined;
-}
-
-type UpdateUserAccountProps =
- | UpgradeUserAccountProps
- | UpgradeUserAccountByEmailProps
- | DowngradeUserAccountProps
- | DowngradeUserAccountByEmailProps
- | ExtendProPlanProps;
-
-// Type Assertion functions
-function isUpgradeUserAccountProps(
- props: UpdateUserAccountProps,
-): props is UpgradeUserAccountProps {
- return (
- props.upgrade === true &&
- 'userId' in props &&
- typeof props.userId === 'string' &&
- (props.proPlanExpirationDate instanceof Date ||
- props.proPlanExpirationDate === undefined)
- );
-}
-
-function isUpgradeUserAccountByEmailProps(
- props: UpdateUserAccountProps,
-): props is UpgradeUserAccountByEmailProps {
- return (
- props.upgrade === true &&
- 'email' in props &&
- typeof props.email === 'string' &&
- (props.proPlanExpirationDate instanceof Date ||
- props.proPlanExpirationDate === undefined)
- );
-}
-
-function isDowngradeUserAccountProps(
- props: UpdateUserAccountProps,
-): props is DowngradeUserAccountProps {
- return (
- props.upgrade === false &&
- 'stripeSubscriptionId' in props &&
- typeof props.stripeSubscriptionId === 'string' &&
- (props.proPlanExpirationDate === undefined ||
- props.proPlanExpirationDate instanceof Date)
- );
-}
-
-function isDowngradeUserAccountByEmailProps(
- props: UpdateUserAccountProps,
-): props is DowngradeUserAccountByEmailProps {
- return (
- props.upgrade === false &&
- 'email' in props &&
- typeof props.email === 'string' &&
- (props.proPlanExpirationDate === undefined ||
- props.proPlanExpirationDate instanceof Date)
- );
-}
-
-function isExtendProPlanProps(
- props: UpdateUserAccountProps,
-): props is ExtendProPlanProps {
- return (
- (props.upgrade === true &&
- typeof props.stripeSubscriptionId === 'string' &&
- props.proPlanExpirationDate instanceof Date) ||
- props.proPlanExpirationDate === undefined
- );
-}