Merge remote-tracking branch 'origin/main'

This commit is contained in:
Weblate
2026-09-25 18:13:02 +00:00
4 changed files with 59 additions and 2 deletions
@@ -14,7 +14,7 @@ import {createRequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import {addGiftCodeDuration} from '@app/api/models/GiftCode';
import type {User} from '@app/api/models/User';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import {createPremiumClearPatch, getEffectivePremiumUntil} from '@app/api/user/UserHelpers';
import {clearPerksSanitizedFlag, createPremiumClearPatch, getEffectivePremiumUntil} from '@app/api/user/UserHelpers';
import {mapUserToPrivateResponse} from '@app/api/user/UserMappers';
import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError';
@@ -62,6 +62,7 @@ export class StripePremiumService {
premium_will_cancel: false,
premium_billing_cycle: billingCycle,
premium_grace_ends_at: null,
premium_flags: clearPerksSanitizedFlag(user.premiumFlags),
},
user.toRow(),
);
@@ -90,6 +91,7 @@ export class StripePremiumService {
premium_since: this.resolvePremiumSince(user.premiumSince, premiumSinceAnchor, now),
premium_until: null,
premium_lifetime_sequence: visionarySequence,
premium_flags: clearPerksSanitizedFlag(user.premiumFlags),
has_ever_purchased: hasEverPurchased,
premium_will_cancel: false,
premium_billing_cycle: null,
@@ -127,6 +129,7 @@ export class StripePremiumService {
};
if ((user.premiumType ?? 0) <= 0) {
patch.premium_type = premiumType;
patch.premium_flags = clearPerksSanitizedFlag(user.premiumFlags);
patch.premium_since = this.resolvePremiumSince(user.premiumSince, null, now);
}
if (hasEverPurchased && !user.hasEverPurchased) {
+4
View File
@@ -275,6 +275,10 @@ export function createPremiumClearPatch(): Partial<UserRow> {
return mapExpiredPremiumFields(() => null) as Partial<UserRow>;
}
export function clearPerksSanitizedFlag(premiumFlags: number): number {
return premiumFlags & ~PremiumFlags.PERKS_SANITIZED;
}
const PROFILE_SUBSTRING_EXEMPT_FLAGS = UserFlags.STAFF;
export function isProfileSubstringExempt(user: Pick<PremiumCheckable, 'flags'>): boolean {
@@ -13,7 +13,7 @@ import {
getSubscriptionPremiumPeriodEnd,
getSubscriptionStartDate,
} from '@app/api/stripe/StripeSubscriptionPeriod';
import {createPremiumClearPatch, getEffectivePremiumUntil} from '@app/api/user/UserHelpers';
import {clearPerksSanitizedFlag, createPremiumClearPatch, getEffectivePremiumUntil} from '@app/api/user/UserHelpers';
import {mapUserToPrivateResponse} from '@app/api/user/UserMappers';
import {getWorkerDependencies} from '@app/api/worker/WorkerContext';
import {PremiumFlags, UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
@@ -73,6 +73,10 @@ function buildStripePremiumRepairPatch(user: User, subscription: Stripe.Subscrip
if (user.stripeSubscriptionId !== subscription.id) {
patch.stripe_subscription_id = subscription.id;
}
const clearedPremiumFlags = clearPerksSanitizedFlag(user.premiumFlags);
if (user.premiumFlags !== clearedPremiumFlags) {
patch.premium_flags = clearedPremiumFlags;
}
if (subscriptionCustomerId && user.stripeCustomerId !== subscriptionCustomerId) {
patch.stripe_customer_id = subscriptionCustomerId;
}
@@ -10,6 +10,7 @@ import {NoopLogger} from '@app/api/test/mocks/NoopLogger';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import processPremiumStateReconciliationQueue from '@app/api/worker/tasks/ProcessPremiumStateReconciliationQueue';
import {clearWorkerDependencies, setWorkerDependenciesForTest} from '@app/api/worker/WorkerContext';
import {PremiumFlags} from '@fluxer/constants/src/UserConstants';
import type {WorkerTaskHelpers} from '@pkgs/worker/src/contracts/WorkerTask';
import type Stripe from 'stripe';
import {afterEach, describe, expect, test} from 'vitest';
@@ -48,6 +49,28 @@ function createCancelledSubscription(endedAtMs: number): Stripe.Subscription {
} as unknown as Stripe.Subscription;
}
function createActiveSubscription(periodEndMs: number): Stripe.Subscription {
return {
id: 'sub_test',
status: 'active',
customer: 'cus_test',
ended_at: null,
canceled_at: null,
cancel_at: null,
cancel_at_period_end: false,
trial_end: null,
start_date: Math.floor((Date.now() - 200 * ONE_DAY_MS) / 1000),
items: {
data: [
{
current_period_end: Math.floor(periodEndMs / 1000),
price: {recurring: {interval: 'month'}},
},
],
},
} as unknown as Stripe.Subscription;
}
function createPaidInvoice(periodEndMs: number): Stripe.Invoice {
return {
id: 'in_test',
@@ -299,4 +322,27 @@ describe('processPremiumStateReconciliationQueue', () => {
expect(patches[0].premium_until).toBeNull();
expect(patches[0].premium_since).toBeNull();
});
test('clears the perks-sanitized latch once the subscription is active again', async () => {
const queueService = createQueueService();
await queueService.enqueueUser(USER_ID, new Date(Date.now() - 1000));
const periodEndMs = Math.floor((Date.now() + 20 * ONE_DAY_MS) / 1000) * 1000;
const user = createPremiumUser({
premium_until: new Date(periodEndMs),
premium_flags: PremiumFlags.PERKS_SANITIZED,
});
const {userRepository, patches, extras} = createCapturingDeps(user);
setWorkerDependenciesForTest({
premiumStateReconciliationQueueService: queueService,
stripe: createStripeStub(createActiveSubscription(periodEndMs), []),
userRepository,
...extras,
});
await processPremiumStateReconciliationQueue({}, createHelpers());
expect(patches).toHaveLength(1);
expect(patches[0].premium_flags).toBe(0);
});
});