fix(server): dirty data handle (#15034)

#### PR Dependency Tree


* **PR #15034** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Consolidated subscription visibility and “active” selection logic so
all subscription queries use a shared, consistent filter across the
platform.

* **Tests**
* Added a test to ensure expired subscriptions are excluded from active
subscription results.
* Updated test fixtures to differentiate expired, unexpired, and onetime
subscriptions for more accurate coverage.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/toeverything/AFFiNE/pull/15034?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-05-28 15:20:17 +08:00
committed by GitHub
parent 66a6a5fffc
commit 1d08e1d8c0
7 changed files with 102 additions and 49 deletions
@@ -1043,3 +1043,44 @@ test('should refresh user subscriptions (empty / revenuecat / stripe-only)', asy
t.is(subs.length, 1, 'case3: only stripe subscription returned');
}
});
test('user subscriptions ignore active rows after their current period ended', async t => {
const { db, subResolver } = t.context;
await db.subscription.createMany({
data: [
{
targetId: user.id,
plan: 'ai',
provider: 'stripe',
status: 'active',
recurring: 'yearly',
start: new Date('2025-01-01T00:00:00.000Z'),
end: new Date('2025-01-08T00:00:00.000Z'),
stripeSubscriptionId: 'sub_expired_ai',
},
{
targetId: user.id,
plan: 'pro',
provider: 'stripe',
status: 'active',
recurring: 'yearly',
start: new Date('2025-01-01T00:00:00.000Z'),
end: new Date('2099-01-01T00:00:00.000Z'),
stripeSubscriptionId: 'sub_current_pro',
},
],
});
const subscriptions = await subResolver.subscriptions(user, user);
t.deepEqual(subscriptions.map(subscription => subscription.plan).sort(), [
'pro',
]);
const manager = t.context.module.get(UserSubscriptionManager);
const activeAI = await manager.getActiveSubscription({
userId: user.id,
plan: SubscriptionPlan.AI,
});
t.is(activeAI, null);
});
@@ -420,7 +420,7 @@ test('should throw if user has subscription already', async t => {
recurring: SubscriptionRecurring.Monthly,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(),
end: new Date(Date.now() + 100000),
},
});
@@ -848,7 +848,7 @@ test('should be able to cancel subscription', async t => {
recurring: SubscriptionRecurring.Yearly,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(),
end: new Date(Date.now() + 100000),
},
});
@@ -1368,7 +1368,7 @@ test('should be able to subscribe to lifetime recurring with old subscription',
recurring: SubscriptionRecurring.Monthly,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(),
end: new Date(Date.now() + 100000),
},
});
@@ -1402,7 +1402,7 @@ test('should not be able to cancel lifetime subscription', async t => {
recurring: SubscriptionRecurring.Lifetime,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(),
end: null,
},
});
@@ -1426,7 +1426,7 @@ test('should not be able to update lifetime recurring', async t => {
recurring: SubscriptionRecurring.Lifetime,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(),
end: null,
},
});
@@ -1481,7 +1481,7 @@ test('should be able to checkout onetime payment if previous subscription is one
variant: SubscriptionVariant.Onetime,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(),
end: new Date(Date.now() + 100000),
},
});
@@ -1518,7 +1518,7 @@ test('should not be able to checkout out onetime payment if previous subscriptio
recurring: SubscriptionRecurring.Monthly,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(),
end: new Date(Date.now() + 100000),
},
});
@@ -1698,7 +1698,7 @@ test('should not be able to checkout for workspace if subscribed', async t => {
recurring: SubscriptionRecurring.Monthly,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(),
end: new Date(Date.now() + 100000),
quantity: 1,
},
});
@@ -1,4 +1,4 @@
import { PrismaClient, UserStripeCustomer } from '@prisma/client';
import { type Prisma, PrismaClient, UserStripeCustomer } from '@prisma/client';
import Stripe from 'stripe';
import { z } from 'zod';
@@ -13,9 +13,40 @@ import {
LookupKey,
SubscriptionPlan,
SubscriptionRecurring,
SubscriptionStatus,
SubscriptionVariant,
} from '../types';
export function validSubscriptionPeriodWhere(
now = new Date()
): Prisma.SubscriptionWhereInput {
return { OR: [{ end: null }, { end: { gt: now } }] };
}
export function activeSubscriptionWhere(
now = new Date()
): Prisma.SubscriptionWhereInput {
return {
status: { in: [SubscriptionStatus.Active, SubscriptionStatus.Trialing] },
...validSubscriptionPeriodWhere(now),
};
}
export function visibleSubscriptionWhere(
now = new Date()
): Prisma.SubscriptionWhereInput {
return {
status: {
in: [
SubscriptionStatus.Active,
SubscriptionStatus.Trialing,
SubscriptionStatus.PastDue,
],
},
...validSubscriptionPeriodWhere(now),
};
}
export interface Subscription {
stripeSubscriptionId: string | null;
stripeScheduleId: string | null;
@@ -15,9 +15,9 @@ import {
LookupKey,
SubscriptionPlan,
SubscriptionRecurring,
SubscriptionStatus,
} from '../types';
import {
activeSubscriptionWhere,
CheckoutParams,
Invoice,
Subscription,
@@ -199,9 +199,7 @@ export class SelfhostTeamSubscriptionManager extends SubscriptionManager {
where: {
targetId: identity.key,
plan: identity.plan,
status: {
in: [SubscriptionStatus.Active, SubscriptionStatus.Trialing],
},
...activeSubscriptionWhere(),
},
});
}
@@ -34,7 +34,12 @@ import {
SubscriptionStatus,
SubscriptionVariant,
} from '../types';
import { CheckoutParams, Subscription, SubscriptionManager } from './common';
import {
activeSubscriptionWhere,
CheckoutParams,
Subscription,
SubscriptionManager,
} from './common';
interface PriceStrategyStatus {
proEarlyAccess: boolean;
@@ -224,9 +229,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
where: {
targetId: args.userId,
plan: args.plan,
status: {
in: [SubscriptionStatus.Active, SubscriptionStatus.Trialing],
},
...activeSubscriptionWhere(),
},
});
}
@@ -24,6 +24,7 @@ import {
SubscriptionStatus,
} from '../types';
import {
activeSubscriptionWhere,
CheckoutParams,
Invoice,
Subscription,
@@ -225,9 +226,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
return this.db.subscription.findFirst({
where: {
targetId: identity.workspaceId,
status: {
in: [SubscriptionStatus.Active, SubscriptionStatus.Trialing],
},
...activeSubscriptionWhere(),
},
});
}
@@ -30,7 +30,12 @@ import { CurrentUser, Public } from '../../core/auth';
import { PermissionAccess } from '../../core/permission';
import { UserType } from '../../core/user';
import { WorkspaceType } from '../../core/workspaces';
import { Invoice, Subscription, WorkspaceSubscriptionManager } from './manager';
import {
Invoice,
Subscription,
visibleSubscriptionWhere,
WorkspaceSubscriptionManager,
} from './manager';
import { RevenueCatWebhookHandler } from './revenuecat';
import { CheckoutParams, SubscriptionService } from './service';
import {
@@ -493,13 +498,7 @@ export class UserSubscriptionResolver {
const subscriptions = await this.db.subscription.findMany({
where: {
targetId: user.id,
status: {
in: [
SubscriptionStatus.Active,
SubscriptionStatus.Trialing,
SubscriptionStatus.PastDue,
],
},
...visibleSubscriptionWhere(),
},
});
@@ -577,13 +576,7 @@ export class UserSubscriptionResolver {
current = await this.db.subscription.findMany({
where: {
targetId: user.id,
status: {
in: [
SubscriptionStatus.Active,
SubscriptionStatus.Trialing,
SubscriptionStatus.PastDue,
],
},
...visibleSubscriptionWhere(),
},
});
// ignore errors
@@ -608,13 +601,7 @@ export class UserSubscriptionResolver {
let current = await this.db.subscription.findMany({
where: {
targetId: user.id,
status: {
in: [
SubscriptionStatus.Active,
SubscriptionStatus.Trialing,
SubscriptionStatus.PastDue,
],
},
...visibleSubscriptionWhere(),
},
});
@@ -641,13 +628,7 @@ export class UserSubscriptionResolver {
current = await this.db.subscription.findMany({
where: {
targetId: user.id,
status: {
in: [
SubscriptionStatus.Active,
SubscriptionStatus.Trialing,
SubscriptionStatus.PastDue,
],
},
...visibleSubscriptionWhere(),
},
});
// ignore errors