fix(server): should auto apply ea price for users (#9082)

This commit is contained in:
forehalo
2024-12-10 05:31:19 +00:00
parent 36a95463b4
commit 564faa439a
6 changed files with 78 additions and 55 deletions
@@ -66,7 +66,7 @@ export abstract class SubscriptionManager {
): KnownStripePrice[] | Promise<KnownStripePrice[]>; ): KnownStripePrice[] | Promise<KnownStripePrice[]>;
abstract checkout( abstract checkout(
price: KnownStripePrice, lookupKey: LookupKey,
params: z.infer<typeof CheckoutParams>, params: z.infer<typeof CheckoutParams>,
args: any args: any
): Promise<Stripe.Checkout.Session>; ): Promise<Stripe.Checkout.Session>;
@@ -206,9 +206,7 @@ export abstract class SubscriptionManager {
return customer; return customer;
} }
protected async getPrice( async getPrice(lookupKey: LookupKey): Promise<KnownStripePrice | null> {
lookupKey: LookupKey
): Promise<KnownStripePrice | null> {
const prices = await this.stripe.prices.list({ const prices = await this.stripe.prices.list({
lookup_keys: [encodeLookupKey(lookupKey)], lookup_keys: [encodeLookupKey(lookupKey)],
limit: 1, limit: 1,
@@ -12,6 +12,7 @@ import {
Config, Config,
EventEmitter, EventEmitter,
InternalServerError, InternalServerError,
InvalidCheckoutParameters,
SubscriptionAlreadyExists, SubscriptionAlreadyExists,
SubscriptionPlanNotFound, SubscriptionPlanNotFound,
URLHelper, URLHelper,
@@ -21,6 +22,7 @@ import {
KnownStripeInvoice, KnownStripeInvoice,
KnownStripePrice, KnownStripePrice,
KnownStripeSubscription, KnownStripeSubscription,
LookupKey,
retriveLookupKeyFromStripeSubscription, retriveLookupKeyFromStripeSubscription,
SubscriptionPlan, SubscriptionPlan,
SubscriptionRecurring, SubscriptionRecurring,
@@ -88,15 +90,20 @@ export class UserSubscriptionManager extends SubscriptionManager {
} }
async checkout( async checkout(
price: KnownStripePrice, lookupKey: LookupKey,
params: z.infer<typeof CheckoutParams>, params: z.infer<typeof CheckoutParams>,
{ user }: z.infer<typeof UserSubscriptionCheckoutArgs> { user }: z.infer<typeof UserSubscriptionCheckoutArgs>
) { ) {
const lookupKey = price.lookupKey; if (
lookupKey.plan !== SubscriptionPlan.Pro &&
lookupKey.plan !== SubscriptionPlan.AI
) {
throw new InvalidCheckoutParameters();
}
const subscription = await this.getSubscription({ const subscription = await this.getSubscription({
// @ts-expect-error filtered already plan: lookupKey.plan,
plan: price.lookupKey.plan, userId: user.id,
user,
}); });
if ( if (
@@ -119,12 +126,12 @@ export class UserSubscriptionManager extends SubscriptionManager {
const customer = await this.getOrCreateCustomer(user.id); const customer = await this.getOrCreateCustomer(user.id);
const strategy = await this.strategyStatus(customer); const strategy = await this.strategyStatus(customer);
const available = await this.isPriceAvailable(price, { const price = await this.autoPrice(lookupKey, strategy);
...strategy,
onetime: true,
});
if (!available) { if (
!price ||
!(await this.isPriceAvailable(price, { ...strategy, onetime: true }))
) {
throw new SubscriptionPlanNotFound({ throw new SubscriptionPlanNotFound({
plan: lookupKey.plan, plan: lookupKey.plan,
recurring: lookupKey.recurring, recurring: lookupKey.recurring,
@@ -564,6 +571,40 @@ export class UserSubscriptionManager extends SubscriptionManager {
return subscription; return subscription;
} }
private async autoPrice(lookupKey: LookupKey, strategy: PriceStrategyStatus) {
// auto select ea variant when available if not specified
let variant: SubscriptionVariant | null = lookupKey.variant;
if (!variant) {
// make the if conditions separated, more readable
// pro early access
if (
lookupKey.plan === SubscriptionPlan.Pro &&
lookupKey.recurring === SubscriptionRecurring.Yearly &&
strategy.proEarlyAccess &&
!strategy.proSubscribed
) {
variant = SubscriptionVariant.EA;
}
// ai early access
if (
lookupKey.plan === SubscriptionPlan.AI &&
lookupKey.recurring === SubscriptionRecurring.Yearly &&
strategy.aiEarlyAccess &&
!strategy.aiSubscribed
) {
variant = SubscriptionVariant.EA;
}
}
return this.getPrice({
plan: lookupKey.plan,
recurring: lookupKey.recurring,
variant,
});
}
private async isPriceAvailable( private async isPriceAvailable(
price: KnownStripePrice, price: KnownStripePrice,
strategy: PriceStrategyStatus strategy: PriceStrategyStatus
@@ -9,12 +9,14 @@ import {
type EventPayload, type EventPayload,
OnEvent, OnEvent,
SubscriptionAlreadyExists, SubscriptionAlreadyExists,
SubscriptionPlanNotFound,
URLHelper, URLHelper,
} from '../../../fundamentals'; } from '../../../fundamentals';
import { import {
KnownStripeInvoice, KnownStripeInvoice,
KnownStripePrice, KnownStripePrice,
KnownStripeSubscription, KnownStripeSubscription,
LookupKey,
retriveLookupKeyFromStripeSubscription, retriveLookupKeyFromStripeSubscription,
SubscriptionPlan, SubscriptionPlan,
SubscriptionRecurring, SubscriptionRecurring,
@@ -62,7 +64,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
} }
async checkout( async checkout(
{ price }: KnownStripePrice, lookupKey: LookupKey,
params: z.infer<typeof CheckoutParams>, params: z.infer<typeof CheckoutParams>,
args: z.infer<typeof WorkspaceSubscriptionCheckoutArgs> args: z.infer<typeof WorkspaceSubscriptionCheckoutArgs>
) { ) {
@@ -75,6 +77,15 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
throw new SubscriptionAlreadyExists({ plan: SubscriptionPlan.Team }); throw new SubscriptionAlreadyExists({ plan: SubscriptionPlan.Team });
} }
const price = await this.getPrice(lookupKey);
if (!price) {
throw new SubscriptionPlanNotFound({
plan: lookupKey.plan,
recurring: lookupKey.recurring,
});
}
const customer = await this.getOrCreateCustomer(args.user.id); const customer = await this.getOrCreateCustomer(args.user.id);
const discounts = await (async () => { const discounts = await (async () => {
@@ -102,7 +113,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
return this.stripe.checkout.sessions.create({ return this.stripe.checkout.sessions.create({
line_items: [ line_items: [
{ {
price: price.id, price: price.price.id,
quantity: count, quantity: count,
}, },
], ],
@@ -42,11 +42,9 @@ import { ScheduleManager } from './schedule';
import { import {
decodeLookupKey, decodeLookupKey,
DEFAULT_PRICES, DEFAULT_PRICES,
encodeLookupKey,
KnownStripeInvoice, KnownStripeInvoice,
KnownStripePrice, KnownStripePrice,
KnownStripeSubscription, KnownStripeSubscription,
LookupKey,
retriveLookupKeyFromStripePrice, retriveLookupKeyFromStripePrice,
retriveLookupKeyFromStripeSubscription, retriveLookupKeyFromStripeSubscription,
SubscriptionPlan, SubscriptionPlan,
@@ -129,19 +127,6 @@ export class SubscriptionService implements OnApplicationBootstrap {
throw new ActionForbidden(); throw new ActionForbidden();
} }
const price = await this.getPrice({
plan,
recurring,
variant: variant ?? null,
});
if (!price) {
throw new SubscriptionPlanNotFound({
plan,
recurring,
});
}
const manager = this.select(plan); const manager = this.select(plan);
const result = CheckoutExtraArgs.safeParse(args); const result = CheckoutExtraArgs.safeParse(args);
@@ -149,7 +134,15 @@ export class SubscriptionService implements OnApplicationBootstrap {
throw new InvalidCheckoutParameters(); throw new InvalidCheckoutParameters();
} }
return manager.checkout(price, params, args); return manager.checkout(
{
plan,
recurring,
variant: variant ?? null,
},
params,
args
);
} }
async cancelSubscription( async cancelSubscription(
@@ -270,7 +263,7 @@ export class SubscriptionService implements OnApplicationBootstrap {
throw new SameSubscriptionRecurring({ recurring }); throw new SameSubscriptionRecurring({ recurring });
} }
const price = await this.getPrice({ const price = await manager.getPrice({
plan: identity.plan, plan: identity.plan,
recurring, recurring,
variant: null, variant: null,
@@ -469,24 +462,6 @@ export class SubscriptionService implements OnApplicationBootstrap {
.filter(Boolean) as KnownStripePrice[]; .filter(Boolean) as KnownStripePrice[];
} }
private async getPrice(
lookupKey: LookupKey
): Promise<KnownStripePrice | null> {
const prices = await this.stripe.prices.list({
lookup_keys: [encodeLookupKey(lookupKey)],
limit: 1,
});
const price = prices.data[0];
return price
? {
lookupKey,
price,
}
: null;
}
private async parseStripeInvoice( private async parseStripeInvoice(
invoice: Stripe.Invoice invoice: Stripe.Invoice
): Promise<KnownStripeInvoice | null> { ): Promise<KnownStripeInvoice | null> {
@@ -26,14 +26,14 @@ export class StripeWebhook {
@OnStripeEvent('invoice.updated') @OnStripeEvent('invoice.updated')
@OnStripeEvent('invoice.finalization_failed') @OnStripeEvent('invoice.finalization_failed')
@OnStripeEvent('invoice.payment_failed') @OnStripeEvent('invoice.payment_failed')
@OnStripeEvent('invoice.payment_succeeded') @OnStripeEvent('invoice.paid')
async onInvoiceUpdated( async onInvoiceUpdated(
event: event:
| Stripe.InvoiceCreatedEvent | Stripe.InvoiceCreatedEvent
| Stripe.InvoiceUpdatedEvent | Stripe.InvoiceUpdatedEvent
| Stripe.InvoiceFinalizationFailedEvent | Stripe.InvoiceFinalizationFailedEvent
| Stripe.InvoicePaymentFailedEvent | Stripe.InvoicePaymentFailedEvent
| Stripe.InvoicePaymentSucceededEvent | Stripe.InvoicePaidEvent
) { ) {
const invoice = await this.stripe.invoices.retrieve(event.data.object.id); const invoice = await this.stripe.invoices.retrieve(event.data.object.id);
await this.service.saveStripeInvoice(invoice); await this.service.saveStripeInvoice(invoice);
@@ -476,7 +476,6 @@ test('should get correct pro plan price for checking out', async t => {
{ {
plan: SubscriptionPlan.Pro, plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Yearly, recurring: SubscriptionRecurring.Yearly,
variant: SubscriptionVariant.EA,
successCallbackLink: '', successCallbackLink: '',
}, },
{ user: u1 } { user: u1 }
@@ -593,7 +592,6 @@ test('should get correct ai plan price for checking out', async t => {
{ {
plan: SubscriptionPlan.AI, plan: SubscriptionPlan.AI,
recurring: SubscriptionRecurring.Yearly, recurring: SubscriptionRecurring.Yearly,
variant: SubscriptionVariant.EA,
successCallbackLink: '', successCallbackLink: '',
}, },
{ user: u1 } { user: u1 }