mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-27 05:33:10 +03:00
fix cancel subscription edge cases (#4691)
This commit is contained in:
parent
113b20f669
commit
303dade2ef
@ -93,7 +93,7 @@ export class SubscriptionService {
|
||||
});
|
||||
|
||||
if (currentSubscription && currentSubscription.end < new Date()) {
|
||||
throw new Error('User already has a subscription');
|
||||
throw new Error('You already have a subscription');
|
||||
}
|
||||
|
||||
const prices = await this.stripe.prices.list({
|
||||
@ -137,11 +137,11 @@ export class SubscriptionService {
|
||||
});
|
||||
|
||||
if (!user?.subscription) {
|
||||
throw new Error('User has no subscription');
|
||||
throw new Error('You do not have any subscription');
|
||||
}
|
||||
|
||||
if (user.subscription.canceledAt) {
|
||||
throw new Error('User subscription has already been canceled ');
|
||||
throw new Error('Your subscription has already been canceled ');
|
||||
}
|
||||
|
||||
// should release the schedule first
|
||||
@ -174,17 +174,15 @@ export class SubscriptionService {
|
||||
});
|
||||
|
||||
if (!user?.subscription) {
|
||||
throw new Error('User has no subscription');
|
||||
throw new Error('You do not have any subscription');
|
||||
}
|
||||
|
||||
if (!user.subscription.canceledAt) {
|
||||
throw new Error('User subscription is not canceled');
|
||||
throw new Error('Your subscription has not been canceled');
|
||||
}
|
||||
|
||||
if (user.subscription.end < new Date()) {
|
||||
throw new Error(
|
||||
'User subscription has already expired, please checkout again.'
|
||||
);
|
||||
throw new Error('Your subscription is expired, please checkout again.');
|
||||
}
|
||||
|
||||
const subscription = await this.stripe.subscriptions.update(
|
||||
@ -211,11 +209,15 @@ export class SubscriptionService {
|
||||
});
|
||||
|
||||
if (!user?.subscription) {
|
||||
throw new Error('User has no subscription');
|
||||
throw new Error('You do not have any subscription');
|
||||
}
|
||||
|
||||
if (user.subscription.canceledAt) {
|
||||
throw new Error('Your subscription has already been canceled ');
|
||||
}
|
||||
|
||||
if (user.subscription.recurring === recurring) {
|
||||
throw new Error('User has already subscribed to this plan');
|
||||
throw new Error('You have already subscribed to this plan');
|
||||
}
|
||||
|
||||
const prices = await this.stripe.prices.list({
|
||||
@ -344,29 +346,98 @@ export class SubscriptionService {
|
||||
}
|
||||
|
||||
@OnEvent('invoice.created')
|
||||
async onInvoiceCreated(invoice: Stripe.Invoice) {
|
||||
await this.saveInvoice(invoice);
|
||||
}
|
||||
|
||||
@OnEvent('invoice.paid')
|
||||
async onInvoicePaid(invoice: Stripe.Invoice) {
|
||||
await this.saveInvoice(invoice);
|
||||
}
|
||||
|
||||
@OnEvent('invoice.finalization_failed')
|
||||
async onInvoiceFinalizeFailed(invoice: Stripe.Invoice) {
|
||||
await this.saveInvoice(invoice);
|
||||
}
|
||||
|
||||
@OnEvent('invoice.payment_failed')
|
||||
async onInvoicePaymentFailed(invoice: Stripe.Invoice) {
|
||||
await this.saveInvoice(invoice);
|
||||
async saveInvoice(stripeInvoice: Stripe.Invoice) {
|
||||
if (!stripeInvoice.customer) {
|
||||
throw new Error('Unexpected invoice with no customer');
|
||||
}
|
||||
|
||||
const user = await this.retrieveUserFromCustomer(
|
||||
typeof stripeInvoice.customer === 'string'
|
||||
? stripeInvoice.customer
|
||||
: stripeInvoice.customer.id
|
||||
);
|
||||
|
||||
const invoice = await this.db.userInvoice.findUnique({
|
||||
where: {
|
||||
stripeInvoiceId: stripeInvoice.id,
|
||||
},
|
||||
});
|
||||
|
||||
const data: Partial<UserInvoice> = {
|
||||
currency: stripeInvoice.currency,
|
||||
amount: stripeInvoice.total,
|
||||
status: stripeInvoice.status ?? InvoiceStatus.Void,
|
||||
link: stripeInvoice.hosted_invoice_url,
|
||||
};
|
||||
|
||||
// handle payment error
|
||||
if (stripeInvoice.attempt_count > 1) {
|
||||
const paymentIntent = await this.stripe.paymentIntents.retrieve(
|
||||
stripeInvoice.payment_intent as string
|
||||
);
|
||||
|
||||
if (paymentIntent.last_payment_error) {
|
||||
if (paymentIntent.last_payment_error.type === 'card_error') {
|
||||
data.lastPaymentError =
|
||||
paymentIntent.last_payment_error.message ?? 'Failed to pay';
|
||||
} else {
|
||||
data.lastPaymentError = 'Internal Payment error';
|
||||
}
|
||||
}
|
||||
} else if (stripeInvoice.last_finalization_error) {
|
||||
if (stripeInvoice.last_finalization_error.type === 'card_error') {
|
||||
data.lastPaymentError =
|
||||
stripeInvoice.last_finalization_error.message ??
|
||||
'Failed to finalize invoice';
|
||||
} else {
|
||||
data.lastPaymentError = 'Internal Payment error';
|
||||
}
|
||||
}
|
||||
|
||||
// update invoice
|
||||
if (invoice) {
|
||||
await this.db.userInvoice.update({
|
||||
where: {
|
||||
stripeInvoiceId: stripeInvoice.id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
// create invoice
|
||||
const price = stripeInvoice.lines.data[0].price;
|
||||
|
||||
if (!price || price.type !== 'recurring') {
|
||||
throw new Error('Unexpected invoice with no recurring price');
|
||||
}
|
||||
|
||||
await this.db.userInvoice.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
stripeInvoiceId: stripeInvoice.id,
|
||||
plan: SubscriptionPlan.Pro,
|
||||
recurring: price.lookup_key ?? price.id,
|
||||
reason: stripeInvoice.billing_reason ?? 'contact support',
|
||||
...(data as any),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async saveSubscription(
|
||||
user: User,
|
||||
subscription: Stripe.Subscription
|
||||
subscription: Stripe.Subscription,
|
||||
fromWebhook = true
|
||||
): Promise<UserSubscription> {
|
||||
// webhook events may not in sequential order
|
||||
// always fetch the latest subscription and save
|
||||
// see https://stripe.com/docs/webhooks#behaviors
|
||||
if (fromWebhook) {
|
||||
subscription = await this.stripe.subscriptions.retrieve(subscription.id);
|
||||
}
|
||||
|
||||
// get next bill date from upcoming invoice
|
||||
// see https://stripe.com/docs/api/invoices/upcoming
|
||||
let nextBillAt: Date | null = null;
|
||||
@ -375,17 +446,7 @@ export class SubscriptionService {
|
||||
subscription.status === SubscriptionStatus.Trialing) &&
|
||||
!subscription.canceled_at
|
||||
) {
|
||||
try {
|
||||
const nextInvoice = await this.stripe.invoices.retrieveUpcoming({
|
||||
customer: subscription.customer as string,
|
||||
subscription: subscription.id,
|
||||
});
|
||||
|
||||
nextBillAt = new Date(nextInvoice.created * 1000);
|
||||
} catch (e) {
|
||||
// no upcoming invoice
|
||||
// safe to ignore
|
||||
}
|
||||
nextBillAt = new Date(subscription.current_period_end * 1000);
|
||||
}
|
||||
|
||||
const price = subscription.items.data[0].price;
|
||||
@ -522,79 +583,4 @@ export class SubscriptionService {
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private async saveInvoice(stripeInvoice: Stripe.Invoice) {
|
||||
if (!stripeInvoice.customer) {
|
||||
throw new Error('Unexpected invoice with no customer');
|
||||
}
|
||||
|
||||
const user = await this.retrieveUserFromCustomer(
|
||||
stripeInvoice.customer as string
|
||||
);
|
||||
|
||||
const invoice = await this.db.userInvoice.findUnique({
|
||||
where: {
|
||||
stripeInvoiceId: stripeInvoice.id,
|
||||
},
|
||||
});
|
||||
|
||||
const data: Partial<UserInvoice> = {
|
||||
currency: stripeInvoice.currency,
|
||||
amount: stripeInvoice.total,
|
||||
status: stripeInvoice.status ?? InvoiceStatus.Void,
|
||||
link: stripeInvoice.hosted_invoice_url,
|
||||
};
|
||||
|
||||
// handle payment error
|
||||
if (stripeInvoice.attempt_count > 1) {
|
||||
const paymentIntent = await this.stripe.paymentIntents.retrieve(
|
||||
stripeInvoice.payment_intent as string
|
||||
);
|
||||
|
||||
if (paymentIntent.last_payment_error) {
|
||||
if (paymentIntent.last_payment_error.type === 'card_error') {
|
||||
data.lastPaymentError =
|
||||
paymentIntent.last_payment_error.message ?? 'Failed to pay';
|
||||
} else {
|
||||
data.lastPaymentError = 'Internal Payment error';
|
||||
}
|
||||
}
|
||||
} else if (stripeInvoice.last_finalization_error) {
|
||||
if (stripeInvoice.last_finalization_error.type === 'card_error') {
|
||||
data.lastPaymentError =
|
||||
stripeInvoice.last_finalization_error.message ??
|
||||
'Failed to finalize invoice';
|
||||
} else {
|
||||
data.lastPaymentError = 'Internal Payment error';
|
||||
}
|
||||
}
|
||||
|
||||
// update invoice
|
||||
if (invoice) {
|
||||
await this.db.userInvoice.update({
|
||||
where: {
|
||||
stripeInvoiceId: stripeInvoice.id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
// create invoice
|
||||
const price = stripeInvoice.lines.data[0].price;
|
||||
|
||||
if (!price || price.type !== 'recurring') {
|
||||
throw new Error('Unexpected invoice with no recurring price');
|
||||
}
|
||||
|
||||
await this.db.userInvoice.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
stripeInvoiceId: stripeInvoice.id,
|
||||
plan: SubscriptionPlan.Pro,
|
||||
recurring: price.lookup_key ?? price.id,
|
||||
reason: stripeInvoice.billing_reason ?? 'contact support',
|
||||
...(data as any),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user