feat: improve event handle (#14177)

This commit is contained in:
DarkSky
2025-12-29 18:54:59 +08:00
committed by GitHub
parent 20a80015c0
commit 6951f1002f
8 changed files with 465 additions and 8 deletions
@@ -55,6 +55,7 @@ import {
SubscriptionPlan,
SubscriptionRecurring,
SubscriptionStatus,
SubscriptionVariant,
} from './types';
export const CheckoutExtraArgs = z.union([
@@ -509,6 +510,128 @@ export class SubscriptionService {
await manager.deleteStripeSubscription(knownSubscription);
}
async handleRefundedInvoice(
invoiceId: string,
reason: 'refund' | 'dispute_open' | 'dispute_lost' | 'dispute_won'
) {
try {
const invoice = await this.stripe.invoices.retrieve(invoiceId, {
expand: ['subscription', 'customer', 'lines.data.price'],
});
const knownInvoice = await this.parseStripeInvoice(invoice);
if (!knownInvoice) {
this.logger.warn(
`Skip handling ${reason}: unable to parse invoice ${invoiceId}`
);
return;
}
const cancelOnStripe = reason === 'refund' || reason === 'dispute_lost';
const revokeLocal =
reason === 'refund' ||
reason === 'dispute_open' ||
reason === 'dispute_lost';
const restore = reason === 'dispute_won';
const isOneTimeOrLifetime =
knownInvoice.lookupKey.recurring === SubscriptionRecurring.Lifetime ||
knownInvoice.lookupKey.variant === SubscriptionVariant.Onetime;
if (restore) {
if (invoice.subscription) {
const subscription =
typeof invoice.subscription === 'string'
? await this.stripe.subscriptions.retrieve(invoice.subscription, {
expand: ['customer'],
})
: invoice.subscription;
const knownSubscription =
await this.parseStripeSubscription(subscription);
if (!knownSubscription) {
this.logger.warn(
`Skip restore: unable to parse subscription ${invoice.subscription} from invoice ${invoiceId}`
);
return;
}
await this.saveStripeSubscription(subscription);
return;
}
if (
isOneTimeOrLifetime &&
(knownInvoice.lookupKey.plan === SubscriptionPlan.Pro ||
knownInvoice.lookupKey.plan === SubscriptionPlan.AI)
) {
await this.userManager.restoreOnetimeOrLifetime(knownInvoice);
}
return;
}
if (!revokeLocal) {
return;
}
if (invoice.subscription) {
const subscription =
typeof invoice.subscription === 'string'
? await this.stripe.subscriptions.retrieve(invoice.subscription, {
expand: ['customer'],
})
: invoice.subscription;
const knownSubscription =
await this.parseStripeSubscription(subscription);
if (!knownSubscription) {
this.logger.warn(
`Skip handling ${reason}: unable to parse subscription ${invoice.subscription} from invoice ${invoiceId}`
);
return;
}
if (cancelOnStripe) {
try {
await this.stripe.subscriptions.cancel(
knownSubscription.stripeSubscription.id
);
} catch (e) {
this.logger.warn(
`Failed to cancel refunded subscription on Stripe ${knownSubscription.stripeSubscription.id}`,
e
);
}
}
const manager = this.select(knownSubscription.lookupKey.plan);
await manager.deleteStripeSubscription(knownSubscription);
return;
}
if (
isOneTimeOrLifetime &&
(knownInvoice.lookupKey.plan === SubscriptionPlan.Pro ||
knownInvoice.lookupKey.plan === SubscriptionPlan.AI)
) {
await this.userManager.revokeOnetimeOrLifetime(knownInvoice);
return;
}
this.logger.warn(
`Handled ${reason} for invoice ${invoiceId}, but no local subscription to cancel`
);
} catch (e) {
this.logger.error(
`Failed to handle ${reason} for invoice ${invoiceId}`,
e
);
}
}
async getOrCreateCustomer({
userId,
userEmail,
@@ -618,8 +741,16 @@ export class SubscriptionService {
private async parseStripeInvoice(
invoice: Stripe.Invoice
): Promise<KnownStripeInvoice | null> {
const customerEmail =
invoice.customer_email ??
(typeof invoice.customer !== 'string' &&
invoice.customer &&
!invoice.customer.deleted
? (invoice.customer.email ?? null)
: null);
// we can't do anything if we can't recognize the customer
if (!invoice.customer_email) {
if (!customerEmail) {
return null;
}
@@ -638,11 +769,11 @@ export class SubscriptionService {
return null;
}
const user = await this.models.user.getUserByEmail(invoice.customer_email);
const user = await this.models.user.getUserByEmail(customerEmail);
return {
userId: user?.id,
userEmail: invoice.customer_email,
userEmail: customerEmail,
stripeInvoice: invoice,
lookupKey,
metadata: invoice.subscription_details?.metadata ?? {},