-
-
Notifications
You must be signed in to change notification settings - Fork 11.5k
Added gift subscription refund handling via Stripe charge.refunded webhook
#27321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -140,4 +140,15 @@ export class Gift { | |
|
|
||
| return {redeemable: true}; | ||
| } | ||
|
|
||
| markRefunded(): boolean { | ||
| if (this.isRefunded()) { | ||
| return false; | ||
| } | ||
|
|
||
| this.status = 'refunded'; | ||
| this.refundedAt = new Date(); | ||
|
|
||
| return true; | ||
|
Comment on lines
+144
to
+152
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refunds can leave the gift in conflicting terminal states.
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| const logging = require('@tryghost/logging'); | ||
|
|
||
| /** | ||
| * Handles `charge.refunded` webhook events | ||
| * | ||
| * When a charge is refunded in Stripe, this service delegates to the | ||
| * appropriate handler based on the type of charge. | ||
| */ | ||
| module.exports = class ChargeRefundedEventService { | ||
| /** | ||
| * @param {object} deps | ||
| * @param {object} deps.giftService | ||
| */ | ||
| constructor(deps) { | ||
| this.deps = deps; | ||
| } | ||
|
|
||
| /** | ||
| * Handles a `charge.refunded` event | ||
| * | ||
| * Extracts the payment intent ID from the charge and delegates to | ||
| * type-specific handlers. | ||
| * | ||
| * @param {import('stripe').Stripe.Charge} charge | ||
| */ | ||
| async handleEvent(charge) { | ||
| // payment_intent can be a string ID or an expanded PaymentIntent object | ||
| const raw = charge.payment_intent; | ||
| const paymentIntentId = typeof raw === 'string' ? raw : raw?.id; | ||
|
|
||
| if (!paymentIntentId) { | ||
| logging.info('charge.refunded: no payment_intent on charge, skipping'); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| // One-time payments (gifts, donations) have no invoice | ||
| if (charge.invoice === null) { | ||
| await this.handleGiftRefundEvent(paymentIntentId); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handles a refund for a gift subscription purchase | ||
| * | ||
| * Looks up the gift by payment intent ID and marks it as refunded. | ||
| * If no gift matches, the refund is for something else and is ignored. | ||
| * | ||
| * @param {string} paymentIntentId | ||
| * @private | ||
| */ | ||
| async handleGiftRefundEvent(paymentIntentId) { | ||
| const refunded = await this.deps.giftService.refundGift(paymentIntentId); | ||
|
|
||
| if (!refunded) { | ||
| logging.info(`charge.refunded: no gift found for payment_intent ${paymentIntentId}, skipping`); | ||
| } | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refunded redeemed gifts can keep paid access (missing downgrade path).
Line 185 leaves a functional gap: if a gift was already redeemed/consumed, refunding only the gift record can leave member entitlement inconsistent with billing state. Please implement the downgrade/reconciliation path (with tests) before release.
I can help draft the downgrade flow and the corresponding unit/e2e test cases if you want me to open a follow-up issue outline.
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis
[warning] 185-185: Complete the task associated to this "TODO" comment.
See more on https://sonarcloud.io/project/issues?id=TryGhost_Ghost&issues=AZ1z8BW7-rAd27ODl7tT&open=AZ1z8BW7-rAd27ODl7tT&pullRequest=27321
🤖 Prompt for AI Agents