fix(server): member loading (#15156)

#### PR Dependency Tree


* **PR #15156** 👈

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

* **Bug Fixes**
* Fixed Stripe subscription syncing for Team plans to update the correct
existing local subscription (avoiding duplicates) while refreshing
quantity and billing/trial period details.
* **UI/UX Improvements**
* Improved workspace member list loading/error states with shared UI
components and steadier pagination behavior.
  * Refined fallback styling for cleaner, more stable layout.
* **Tests**
* Expanded subscription and projection coverage and adjusted
seat-allocation/e2e assertions to be more robust.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-06-26 21:29:15 +08:00
committed by GitHub
parent 8e036a2f38
commit 4a7c931eca
11 changed files with 248 additions and 110 deletions
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client';
import { omit, pick } from 'lodash-es';
import { omit } from 'lodash-es';
import { z } from 'zod';
import { SubscriptionPlanNotFound, URLHelper } from '../../../base';
@@ -163,13 +163,14 @@ export class SelfhostTeamSubscriptionManager extends SubscriptionManager {
where: {
stripeSubscriptionId: stripeSubscription.id,
},
data: pick(subscriptionData, [
'status',
'stripeScheduleId',
'nextBillAt',
'canceledAt',
'end',
]),
data: {
...omit(subscriptionData, ['provider', 'iapStore']),
provider: Provider.stripe,
iapStore: null,
rcEntitlement: null,
rcProductId: null,
rcExternalRef: null,
},
});
await this.upsertStripeProviderSubscription(
saved.targetId,
@@ -5,7 +5,7 @@ import {
Provider,
UserStripeCustomer,
} from '@prisma/client';
import { omit, pick } from 'lodash-es';
import { omit } from 'lodash-es';
import Stripe from 'stripe';
import { z } from 'zod';
@@ -272,13 +272,14 @@ export class UserSubscriptionManager extends SubscriptionManager {
const saved = existingByStripeId
? await this.db.subscription.update({
where: { id: existingByStripeId.id },
data: pick(subscriptionData, [
'status',
'stripeScheduleId',
'nextBillAt',
'canceledAt',
'end',
]),
data: {
...omit(subscriptionData, ['provider', 'iapStore']),
provider: Provider.stripe,
iapStore: null,
rcEntitlement: null,
rcProductId: null,
rcExternalRef: null,
},
})
: await this.db.subscription.upsert({
// TODO(stable-upgrade): remove legacy subscriptions dual-write after stable supports provider facts.
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client';
import { omit, pick } from 'lodash-es';
import { omit } from 'lodash-es';
import { z } from 'zod';
import {
@@ -163,28 +163,44 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
});
}
const saved = await this.db.subscription.upsert({
// TODO(stable-upgrade): remove legacy subscriptions dual-write after stable supports provider facts.
// TODO(stable-upgrade): remove reliance on target_id_plan unique slot after contract cleanup.
where: {
provider: Provider.stripe,
stripeSubscriptionId: stripeSubscription.id,
},
update: {
...pick(subscriptionData, [
'status',
'stripeScheduleId',
'nextBillAt',
'canceledAt',
'quantity',
'end',
]),
},
create: {
targetId: workspaceId,
...omit(subscriptionData, 'provider', 'iapStore'),
},
const existingByStripeId = await this.db.subscription.findUnique({
where: { stripeSubscriptionId: stripeSubscription.id },
});
const saved = existingByStripeId
? await this.db.subscription.update({
where: { id: existingByStripeId.id },
data: {
...omit(subscriptionData, ['provider', 'iapStore']),
provider: Provider.stripe,
iapStore: null,
rcEntitlement: null,
rcProductId: null,
rcExternalRef: null,
},
})
: await this.db.subscription.upsert({
// TODO(stable-upgrade): remove legacy subscriptions dual-write after stable supports provider facts.
// TODO(stable-upgrade): remove reliance on target_id_plan unique slot after contract cleanup.
where: {
targetId_plan: {
targetId: workspaceId,
plan: lookupKey.plan,
},
},
update: {
...omit(subscriptionData, ['provider', 'iapStore']),
provider: Provider.stripe,
iapStore: null,
rcEntitlement: null,
rcProductId: null,
rcExternalRef: null,
},
create: {
targetId: workspaceId,
...omit(subscriptionData, 'provider', 'iapStore'),
},
});
await this.entitlement.upsertFromCloudSubscription(saved);
return saved;
}