fix: update stripe team member billing (#2991)

This commit is contained in:
David Nguyen
2026-07-23 13:09:06 +09:00
committed by GitHub
parent 26f0c4c5b7
commit 54befb5962
17 changed files with 674 additions and 309 deletions
@@ -1,8 +1,6 @@
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { prisma } from '@documenso/prisma';
import { OrganisationMemberInviteStatus } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
@@ -28,8 +26,6 @@ export const deleteAdminOrganisationMemberRoute = adminProcedure
id: organisationId,
},
include: {
subscription: true,
organisationClaim: true,
teams: {
select: {
id: true,
@@ -41,14 +37,6 @@ export const deleteAdminOrganisationMemberRoute = adminProcedure
userId: true,
},
},
invites: {
where: {
status: OrganisationMemberInviteStatus.PENDING,
},
select: {
id: true,
},
},
},
});
@@ -72,18 +60,6 @@ export const deleteAdminOrganisationMemberRoute = adminProcedure
});
}
const newMemberCount = organisation.members.length + organisation.invites.length - 1;
// Removing a member is a reducing operation, so we don't gate it on the
// subscription being present. Sync Stripe only when one exists.
if (organisation.subscription) {
await syncMemberCountWithStripeSeatPlan(
organisation.subscription,
organisation.organisationClaim,
newMemberCount,
);
}
const teamIds = organisation.teams.map((team) => team.id);
await prisma.$transaction(async (tx) => {
@@ -113,6 +89,13 @@ export const deleteAdminOrganisationMemberRoute = adminProcedure
});
});
// A member was removed — queue a seat sync to true the Stripe quantity down
// to the new count (no proration, no credit).
await jobs.triggerJob({
name: 'internal.sync-organisation-seats',
payload: { organisationId },
});
await jobs.triggerJob({
name: 'send.organisation-member-left.email',
payload: {
@@ -1,4 +1,3 @@
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getMemberOrganisationRole } from '@documenso/lib/server-only/team/get-member-roles';
@@ -32,20 +31,6 @@ export const deleteOrganisationMemberInvitesRoute = authenticatedProcedure
userId,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
include: {
organisationClaim: true,
subscription: true,
members: {
select: {
id: true,
},
},
invites: {
select: {
id: true,
},
},
},
});
if (!organisation) {
@@ -83,22 +68,6 @@ export const deleteOrganisationMemberInvitesRoute = authenticatedProcedure
});
}
const { organisationClaim } = organisation;
const numberOfCurrentMembers = organisation.members.length;
const numberOfCurrentInvites = organisation.invites.length;
const totalMemberCountWithInvites = numberOfCurrentMembers + numberOfCurrentInvites - 1;
// Removing pending invites is a reducing operation, so we don't gate it on
// the subscription being present. Sync Stripe only when one exists.
if (organisation.subscription) {
await syncMemberCountWithStripeSeatPlan(
organisation.subscription,
organisationClaim,
totalMemberCountWithInvites,
);
}
await prisma.organisationMemberInvite.deleteMany({
where: {
id: {
@@ -1,8 +1,8 @@
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import {
ORGANISATION_MEMBER_ROLE_HIERARCHY,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP,
} from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import {
@@ -11,7 +11,6 @@ import {
isOrganisationRoleWithinUserHierarchy,
} from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { OrganisationMemberInviteStatus } from '@documenso/prisma/client';
import { authenticatedProcedure } from '../trpc';
import {
@@ -59,8 +58,6 @@ export const deleteOrganisationMembers = async ({
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
include: {
subscription: true,
organisationClaim: true,
teams: {
select: {
id: true,
@@ -75,14 +72,6 @@ export const deleteOrganisationMembers = async ({
},
},
},
invites: {
where: {
status: OrganisationMemberInviteStatus.PENDING,
},
select: {
id: true,
},
},
},
});
@@ -90,8 +79,6 @@ export const deleteOrganisationMembers = async ({
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
const { organisationClaim } = organisation;
const membersToDelete = organisation.members.filter((member) => organisationMemberIds.includes(member.id));
const currentUserMember = organisation.members.find((member) => member.userId === userId);
@@ -129,15 +116,6 @@ export const deleteOrganisationMembers = async ({
}
}
const inviteCount = organisation.invites.length;
const newMemberCount = organisation.members.length + inviteCount - membersToDelete.length;
// Removing members is a reducing operation, so we don't gate it on the
// subscription being present. Sync Stripe only when one exists.
if (organisation.subscription) {
await syncMemberCountWithStripeSeatPlan(organisation.subscription, organisationClaim, newMemberCount);
}
const removedUserIds = membersToDelete.map((member) => member.userId);
const teamIds = organisation.teams.map((team) => team.id);
@@ -184,6 +162,13 @@ export const deleteOrganisationMembers = async ({
});
});
// Members were removed — queue a seat sync to true the Stripe quantity down to
// the new count (no proration, no credit).
await jobs.triggerJob({
name: 'internal.sync-organisation-seats',
payload: { organisationId },
});
for (const member of membersToDelete) {
await jobs.triggerJob({
name: 'send.organisation-member-left.email',
@@ -1,9 +1,7 @@
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { OrganisationMemberInviteStatus } from '@documenso/prisma/client';
import { authenticatedProcedure } from '../trpc';
import { ZLeaveOrganisationRequestSchema, ZLeaveOrganisationResponseSchema } from './leave-organisation.types';
@@ -24,26 +22,11 @@ export const leaveOrganisationRoute = authenticatedProcedure
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({ organisationId, userId }),
include: {
organisationClaim: true,
subscription: true,
teams: {
select: {
id: true,
},
},
invites: {
where: {
status: OrganisationMemberInviteStatus.PENDING,
},
select: {
id: true,
},
},
members: {
select: {
id: true,
},
},
},
});
@@ -59,17 +42,6 @@ export const leaveOrganisationRoute = authenticatedProcedure
});
}
const { organisationClaim } = organisation;
const inviteCount = organisation.invites.length;
const newMemberCount = organisation.members.length + inviteCount - 1;
// Leaving is a reducing operation, so we don't gate it on the subscription
// being present. Sync Stripe only when one exists.
if (organisation.subscription) {
await syncMemberCountWithStripeSeatPlan(organisation.subscription, organisationClaim, newMemberCount);
}
const teamIds = organisation.teams.map((team) => team.id);
await prisma.$transaction(async (tx) => {
@@ -101,6 +73,13 @@ export const leaveOrganisationRoute = authenticatedProcedure
});
});
// A member was removed — queue a seat sync to true the Stripe quantity down
// to the new count (no proration, no credit).
await jobs.triggerJob({
name: 'internal.sync-organisation-seats',
payload: { organisationId },
});
await jobs.triggerJob({
name: 'send.organisation-member-left.email',
payload: {