mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 10:31:50 +08:00
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 --> [](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:
@@ -1043,3 +1043,44 @@ test('should refresh user subscriptions (empty / revenuecat / stripe-only)', asy
|
|||||||
t.is(subs.length, 1, 'case3: only stripe subscription returned');
|
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,
|
recurring: SubscriptionRecurring.Monthly,
|
||||||
status: SubscriptionStatus.Active,
|
status: SubscriptionStatus.Active,
|
||||||
start: new Date(),
|
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,
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
status: SubscriptionStatus.Active,
|
status: SubscriptionStatus.Active,
|
||||||
start: new Date(),
|
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,
|
recurring: SubscriptionRecurring.Monthly,
|
||||||
status: SubscriptionStatus.Active,
|
status: SubscriptionStatus.Active,
|
||||||
start: new Date(),
|
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,
|
recurring: SubscriptionRecurring.Lifetime,
|
||||||
status: SubscriptionStatus.Active,
|
status: SubscriptionStatus.Active,
|
||||||
start: new Date(),
|
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,
|
recurring: SubscriptionRecurring.Lifetime,
|
||||||
status: SubscriptionStatus.Active,
|
status: SubscriptionStatus.Active,
|
||||||
start: new Date(),
|
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,
|
variant: SubscriptionVariant.Onetime,
|
||||||
status: SubscriptionStatus.Active,
|
status: SubscriptionStatus.Active,
|
||||||
start: new Date(),
|
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,
|
recurring: SubscriptionRecurring.Monthly,
|
||||||
status: SubscriptionStatus.Active,
|
status: SubscriptionStatus.Active,
|
||||||
start: new Date(),
|
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,
|
recurring: SubscriptionRecurring.Monthly,
|
||||||
status: SubscriptionStatus.Active,
|
status: SubscriptionStatus.Active,
|
||||||
start: new Date(),
|
start: new Date(),
|
||||||
end: new Date(),
|
end: new Date(Date.now() + 100000),
|
||||||
quantity: 1,
|
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 Stripe from 'stripe';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -13,9 +13,40 @@ import {
|
|||||||
LookupKey,
|
LookupKey,
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
|
SubscriptionStatus,
|
||||||
SubscriptionVariant,
|
SubscriptionVariant,
|
||||||
} from '../types';
|
} 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 {
|
export interface Subscription {
|
||||||
stripeSubscriptionId: string | null;
|
stripeSubscriptionId: string | null;
|
||||||
stripeScheduleId: string | null;
|
stripeScheduleId: string | null;
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import {
|
|||||||
LookupKey,
|
LookupKey,
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
SubscriptionStatus,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
|
activeSubscriptionWhere,
|
||||||
CheckoutParams,
|
CheckoutParams,
|
||||||
Invoice,
|
Invoice,
|
||||||
Subscription,
|
Subscription,
|
||||||
@@ -199,9 +199,7 @@ export class SelfhostTeamSubscriptionManager extends SubscriptionManager {
|
|||||||
where: {
|
where: {
|
||||||
targetId: identity.key,
|
targetId: identity.key,
|
||||||
plan: identity.plan,
|
plan: identity.plan,
|
||||||
status: {
|
...activeSubscriptionWhere(),
|
||||||
in: [SubscriptionStatus.Active, SubscriptionStatus.Trialing],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ import {
|
|||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
SubscriptionVariant,
|
SubscriptionVariant,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { CheckoutParams, Subscription, SubscriptionManager } from './common';
|
import {
|
||||||
|
activeSubscriptionWhere,
|
||||||
|
CheckoutParams,
|
||||||
|
Subscription,
|
||||||
|
SubscriptionManager,
|
||||||
|
} from './common';
|
||||||
|
|
||||||
interface PriceStrategyStatus {
|
interface PriceStrategyStatus {
|
||||||
proEarlyAccess: boolean;
|
proEarlyAccess: boolean;
|
||||||
@@ -224,9 +229,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
|
|||||||
where: {
|
where: {
|
||||||
targetId: args.userId,
|
targetId: args.userId,
|
||||||
plan: args.plan,
|
plan: args.plan,
|
||||||
status: {
|
...activeSubscriptionWhere(),
|
||||||
in: [SubscriptionStatus.Active, SubscriptionStatus.Trialing],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
|
activeSubscriptionWhere,
|
||||||
CheckoutParams,
|
CheckoutParams,
|
||||||
Invoice,
|
Invoice,
|
||||||
Subscription,
|
Subscription,
|
||||||
@@ -225,9 +226,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
|
|||||||
return this.db.subscription.findFirst({
|
return this.db.subscription.findFirst({
|
||||||
where: {
|
where: {
|
||||||
targetId: identity.workspaceId,
|
targetId: identity.workspaceId,
|
||||||
status: {
|
...activeSubscriptionWhere(),
|
||||||
in: [SubscriptionStatus.Active, SubscriptionStatus.Trialing],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ import { CurrentUser, Public } from '../../core/auth';
|
|||||||
import { PermissionAccess } from '../../core/permission';
|
import { PermissionAccess } from '../../core/permission';
|
||||||
import { UserType } from '../../core/user';
|
import { UserType } from '../../core/user';
|
||||||
import { WorkspaceType } from '../../core/workspaces';
|
import { WorkspaceType } from '../../core/workspaces';
|
||||||
import { Invoice, Subscription, WorkspaceSubscriptionManager } from './manager';
|
import {
|
||||||
|
Invoice,
|
||||||
|
Subscription,
|
||||||
|
visibleSubscriptionWhere,
|
||||||
|
WorkspaceSubscriptionManager,
|
||||||
|
} from './manager';
|
||||||
import { RevenueCatWebhookHandler } from './revenuecat';
|
import { RevenueCatWebhookHandler } from './revenuecat';
|
||||||
import { CheckoutParams, SubscriptionService } from './service';
|
import { CheckoutParams, SubscriptionService } from './service';
|
||||||
import {
|
import {
|
||||||
@@ -493,13 +498,7 @@ export class UserSubscriptionResolver {
|
|||||||
const subscriptions = await this.db.subscription.findMany({
|
const subscriptions = await this.db.subscription.findMany({
|
||||||
where: {
|
where: {
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
status: {
|
...visibleSubscriptionWhere(),
|
||||||
in: [
|
|
||||||
SubscriptionStatus.Active,
|
|
||||||
SubscriptionStatus.Trialing,
|
|
||||||
SubscriptionStatus.PastDue,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -577,13 +576,7 @@ export class UserSubscriptionResolver {
|
|||||||
current = await this.db.subscription.findMany({
|
current = await this.db.subscription.findMany({
|
||||||
where: {
|
where: {
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
status: {
|
...visibleSubscriptionWhere(),
|
||||||
in: [
|
|
||||||
SubscriptionStatus.Active,
|
|
||||||
SubscriptionStatus.Trialing,
|
|
||||||
SubscriptionStatus.PastDue,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// ignore errors
|
// ignore errors
|
||||||
@@ -608,13 +601,7 @@ export class UserSubscriptionResolver {
|
|||||||
let current = await this.db.subscription.findMany({
|
let current = await this.db.subscription.findMany({
|
||||||
where: {
|
where: {
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
status: {
|
...visibleSubscriptionWhere(),
|
||||||
in: [
|
|
||||||
SubscriptionStatus.Active,
|
|
||||||
SubscriptionStatus.Trialing,
|
|
||||||
SubscriptionStatus.PastDue,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -641,13 +628,7 @@ export class UserSubscriptionResolver {
|
|||||||
current = await this.db.subscription.findMany({
|
current = await this.db.subscription.findMany({
|
||||||
where: {
|
where: {
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
status: {
|
...visibleSubscriptionWhere(),
|
||||||
in: [
|
|
||||||
SubscriptionStatus.Active,
|
|
||||||
SubscriptionStatus.Trialing,
|
|
||||||
SubscriptionStatus.PastDue,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// ignore errors
|
// ignore errors
|
||||||
|
|||||||
Reference in New Issue
Block a user