diff --git a/fluxer_api/src/api/stripe/StripeGiftTrialMetadata.ts b/fluxer_api/src/api/stripe/StripeGiftTrialMetadata.ts index 6054e6cf6..fb5c59f4a 100644 --- a/fluxer_api/src/api/stripe/StripeGiftTrialMetadata.ts +++ b/fluxer_api/src/api/stripe/StripeGiftTrialMetadata.ts @@ -9,7 +9,7 @@ export function getGiftTrialMetadataKey(giftCode: string): string { return `${GIFT_TRIAL_KEY_PREFIX}${giftCode}`; } -function isGiftTrialRunning(subscription: Stripe.Subscription, nowMs: number): boolean { +export function isGiftTrialRunning(subscription: Stripe.Subscription, nowMs: number): boolean { return subscription.trial_end != null && subscription.trial_end * 1000 > nowMs; } diff --git a/fluxer_api/src/api/stripe/services/StripeSubscriptionService.ts b/fluxer_api/src/api/stripe/services/StripeSubscriptionService.ts index b1ab627ee..50d8a5588 100644 --- a/fluxer_api/src/api/stripe/services/StripeSubscriptionService.ts +++ b/fluxer_api/src/api/stripe/services/StripeSubscriptionService.ts @@ -10,7 +10,11 @@ import {addGiftCodeDuration} from '@app/api/models/GiftCode'; import type {User} from '@app/api/models/User'; import type {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService'; import type {ProductInfo, RecurringBillingCycle} from '@app/api/stripe/ProductRegistry'; -import {buildGiftTrialMetadata, getGiftTrialPaidUntil} from '@app/api/stripe/StripeGiftTrialMetadata'; +import { + buildGiftTrialMetadata, + getGiftTrialPaidUntil, + isGiftTrialRunning, +} from '@app/api/stripe/StripeGiftTrialMetadata'; import { getPrimarySubscriptionItem, getSubscriptionEntitlementPeriodEndUnix, @@ -1293,7 +1297,8 @@ export class StripeSubscriptionService { const currentTrialEnd = subscription.trial_end; const item = getPrimarySubscriptionItem(subscription); const currentPeriodEnd = getSubscriptionItemPeriodEndUnix(item); - const baseUnix = currentTrialEnd ?? currentPeriodEnd; + const nowMs = Date.now(); + const baseUnix = isGiftTrialRunning(subscription, nowMs) ? currentTrialEnd : currentPeriodEnd; Logger.debug( { userId: user.id, @@ -1331,7 +1336,7 @@ export class StripeSubscriptionService { { trial_end: newTrialEndUnix, proration_behavior: 'none', - metadata: buildGiftTrialMetadata(subscription, idempotencyKey, baseUnix, newTrialEndUnix, Date.now()), + metadata: buildGiftTrialMetadata(subscription, idempotencyKey, baseUnix, newTrialEndUnix, nowMs), }, {idempotencyKey: stripeIdempotencyKey}, ); diff --git a/fluxer_api/src/api/stripe/tests/StripeGiftSubscriptionShift.test.ts b/fluxer_api/src/api/stripe/tests/StripeGiftSubscriptionShift.test.ts index 9c59fda58..1291e2051 100644 --- a/fluxer_api/src/api/stripe/tests/StripeGiftSubscriptionShift.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeGiftSubscriptionShift.test.ts @@ -749,5 +749,45 @@ describe('gift time around a Stripe subscription', () => { const afterWebhook = await findUser(account.userId); expect(afterWebhook.premiumGiftExtensionEndsAt?.getTime()).toBe(trialEnd.getTime()); }); + + test('stacks from the paid period end when an earlier gift trial has ended', async () => { + const subscriptionId = 'sub_shift_stale_trial'; + const periodEnd = Math.floor((Date.now() + ms('20 days')) / 1000); + const staleTrialEnd = periodEnd - 60 * 24 * 60 * 60; + useStripe({ + subscriptions: { + [subscriptionId]: { + current_period_start: periodEnd - 30 * 24 * 60 * 60, + current_period_end: periodEnd, + trial_end: staleTrialEnd, + metadata: giftTrialMetadata('OLDGIFT', 30, staleTrialEnd - 30 * 24 * 60 * 60), + }, + }, + }); + const {account} = await createUser({ + premium_type: UserPremiumTypes.SUBSCRIPTION, + stripe_subscription_id: subscriptionId, + stripe_customer_id: 'cus_test_1', + premium_billing_cycle: 'monthly', + premium_until: new Date(periodEnd * 1000), + }); + const gifter = await createTestAccount(harness); + await createBuilder(harness, gifter.token) + .post('/test/gifts/SHIFTSTALEGIFT') + .body({duration_type: 'days', duration_quantity: 30, created_by_user_id: gifter.userId}) + .execute(); + + await createBuilder(harness, account.token).post('/gifts/SHIFTSTALEGIFT/redeem').expect(204).execute(); + + const update = stripeHandlers.spies.updatedSubscriptions[0]?.params; + expect(Number(update?.trial_end)).toBe(periodEnd + 30 * 24 * 60 * 60); + expect(update?.metadata).toEqual({ + gtrial_OLDGIFT: '', + gtrial_paid_until: String(periodEnd), + gtrial_SHIFTSTALEGIFT: String(30 * 24 * 60 * 60), + }); + const redeemed = await findUser(account.userId); + expect(redeemed.premiumUntil?.getTime()).toBe((periodEnd + 30 * 24 * 60 * 60) * 1000); + }); }); });