mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(billing): stack gifts from paid period end after trial ends (#3406)
This commit is contained in:
1 parent
bcb30eea88
commit
74c6fad260
3 files changed
+49
-4
No files matched your search
@@ -9,7 +9,7 @@ export function getGiftTrialMetadataKey(giftCode: string): string {
|
|||||||
return `${GIFT_TRIAL_KEY_PREFIX}${giftCode}`;
|
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;
|
return subscription.trial_end != null && subscription.trial_end * 1000 > nowMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {addGiftCodeDuration} from '@app/api/models/GiftCode';
|
|||||||
import type {User} from '@app/api/models/User';
|
import type {User} from '@app/api/models/User';
|
||||||
import type {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService';
|
import type {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService';
|
||||||
import type {ProductInfo, RecurringBillingCycle} from '@app/api/stripe/ProductRegistry';
|
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 {
|
import {
|
||||||
getPrimarySubscriptionItem,
|
getPrimarySubscriptionItem,
|
||||||
getSubscriptionEntitlementPeriodEndUnix,
|
getSubscriptionEntitlementPeriodEndUnix,
|
||||||
@@ -1293,7 +1297,8 @@ export class StripeSubscriptionService {
|
|||||||
const currentTrialEnd = subscription.trial_end;
|
const currentTrialEnd = subscription.trial_end;
|
||||||
const item = getPrimarySubscriptionItem(subscription);
|
const item = getPrimarySubscriptionItem(subscription);
|
||||||
const currentPeriodEnd = getSubscriptionItemPeriodEndUnix(item);
|
const currentPeriodEnd = getSubscriptionItemPeriodEndUnix(item);
|
||||||
const baseUnix = currentTrialEnd ?? currentPeriodEnd;
|
const nowMs = Date.now();
|
||||||
|
const baseUnix = isGiftTrialRunning(subscription, nowMs) ? currentTrialEnd : currentPeriodEnd;
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
{
|
{
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -1331,7 +1336,7 @@ export class StripeSubscriptionService {
|
|||||||
{
|
{
|
||||||
trial_end: newTrialEndUnix,
|
trial_end: newTrialEndUnix,
|
||||||
proration_behavior: 'none',
|
proration_behavior: 'none',
|
||||||
metadata: buildGiftTrialMetadata(subscription, idempotencyKey, baseUnix, newTrialEndUnix, Date.now()),
|
metadata: buildGiftTrialMetadata(subscription, idempotencyKey, baseUnix, newTrialEndUnix, nowMs),
|
||||||
},
|
},
|
||||||
{idempotencyKey: stripeIdempotencyKey},
|
{idempotencyKey: stripeIdempotencyKey},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -749,5 +749,45 @@ describe('gift time around a Stripe subscription', () => {
|
|||||||
const afterWebhook = await findUser(account.userId);
|
const afterWebhook = await findUser(account.userId);
|
||||||
expect(afterWebhook.premiumGiftExtensionEndsAt?.getTime()).toBe(trialEnd.getTime());
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user