Skip to content

Commit 8d0e50f

Browse files
improvement(admin-routes): cleanup code that could accidentally desync stripe and DB (#2363)
* remove non-functional admin route * stripe updates cleanup
1 parent f7d1b06 commit 8d0e50f

File tree

7 files changed

+318
-400
lines changed

7 files changed

+318
-400
lines changed

apps/sim/app/api/v1/admin/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,18 @@
3535
* GET /api/v1/admin/organizations/:id - Get organization details
3636
* PATCH /api/v1/admin/organizations/:id - Update organization
3737
* GET /api/v1/admin/organizations/:id/members - List organization members
38-
* POST /api/v1/admin/organizations/:id/members - Add/update member in organization
38+
* POST /api/v1/admin/organizations/:id/members - Add/update member (validates seat availability)
3939
* GET /api/v1/admin/organizations/:id/members/:mid - Get member details
4040
* PATCH /api/v1/admin/organizations/:id/members/:mid - Update member role
4141
* DELETE /api/v1/admin/organizations/:id/members/:mid - Remove member
4242
* GET /api/v1/admin/organizations/:id/billing - Get org billing summary
4343
* PATCH /api/v1/admin/organizations/:id/billing - Update org usage limit
4444
* GET /api/v1/admin/organizations/:id/seats - Get seat analytics
45-
* PATCH /api/v1/admin/organizations/:id/seats - Update seat count
4645
*
4746
* Subscriptions:
4847
* GET /api/v1/admin/subscriptions - List all subscriptions
4948
* GET /api/v1/admin/subscriptions/:id - Get subscription details
50-
* PATCH /api/v1/admin/subscriptions/:id - Update subscription
49+
* DELETE /api/v1/admin/subscriptions/:id - Cancel subscription (?atPeriodEnd=true for scheduled)
5150
*/
5251

5352
export type { AdminAuthFailure, AdminAuthResult, AdminAuthSuccess } from '@/app/api/v1/admin/auth'

apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
* POST /api/v1/admin/organizations/[id]/members
1313
*
1414
* Add a user to an organization with full billing logic.
15+
* Validates seat availability before adding (uses same logic as invitation flow):
16+
* - Team plans: checks seats column
17+
* - Enterprise plans: checks metadata.seats
1518
* Handles Pro usage snapshot and subscription cancellation like the invitation flow.
1619
* If user is already a member, updates their role if different.
1720
*
@@ -29,6 +32,7 @@ import { db } from '@sim/db'
2932
import { member, organization, user, userStats } from '@sim/db/schema'
3033
import { count, eq } from 'drizzle-orm'
3134
import { addUserToOrganization } from '@/lib/billing/organizations/membership'
35+
import { requireStripeClient } from '@/lib/billing/stripe-client'
3236
import { createLogger } from '@/lib/logs/console/logger'
3337
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
3438
import {
@@ -223,6 +227,29 @@ export const POST = withAdminAuthParams<RouteParams>(async (request, context) =>
223227
return badRequestResponse(result.error || 'Failed to add member')
224228
}
225229

230+
// Sync Pro subscription cancellation with Stripe (same as invitation flow)
231+
if (result.billingActions.proSubscriptionToCancel?.stripeSubscriptionId) {
232+
try {
233+
const stripe = requireStripeClient()
234+
await stripe.subscriptions.update(
235+
result.billingActions.proSubscriptionToCancel.stripeSubscriptionId,
236+
{ cancel_at_period_end: true }
237+
)
238+
logger.info('Admin API: Synced Pro cancellation with Stripe', {
239+
userId: body.userId,
240+
subscriptionId: result.billingActions.proSubscriptionToCancel.subscriptionId,
241+
stripeSubscriptionId: result.billingActions.proSubscriptionToCancel.stripeSubscriptionId,
242+
})
243+
} catch (stripeError) {
244+
logger.error('Admin API: Failed to sync Pro cancellation with Stripe', {
245+
userId: body.userId,
246+
subscriptionId: result.billingActions.proSubscriptionToCancel.subscriptionId,
247+
stripeSubscriptionId: result.billingActions.proSubscriptionToCancel.stripeSubscriptionId,
248+
error: stripeError,
249+
})
250+
}
251+
}
252+
226253
const data: AdminMember = {
227254
id: result.memberId!,
228255
userId: body.userId,

apps/sim/app/api/v1/admin/organizations/[id]/seats/route.ts

Lines changed: 0 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,12 @@
44
* Get organization seat analytics including member activity.
55
*
66
* Response: AdminSingleResponse<AdminSeatAnalytics>
7-
*
8-
* PATCH /api/v1/admin/organizations/[id]/seats
9-
*
10-
* Update organization seat count with Stripe sync (matches user flow).
11-
*
12-
* Body:
13-
* - seats: number - New seat count (positive integer)
14-
*
15-
* Response: AdminSingleResponse<{ success: true, seats: number, plan: string, stripeUpdated?: boolean }>
167
*/
178

18-
import { db } from '@sim/db'
19-
import { organization, subscription } from '@sim/db/schema'
20-
import { and, eq } from 'drizzle-orm'
21-
import { requireStripeClient } from '@/lib/billing/stripe-client'
229
import { getOrganizationSeatAnalytics } from '@/lib/billing/validation/seat-management'
2310
import { createLogger } from '@/lib/logs/console/logger'
2411
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
2512
import {
26-
badRequestResponse,
2713
internalErrorResponse,
2814
notFoundResponse,
2915
singleResponse,
@@ -75,122 +61,3 @@ export const GET = withAdminAuthParams<RouteParams>(async (_, context) => {
7561
return internalErrorResponse('Failed to get organization seats')
7662
}
7763
})
78-
79-
export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) => {
80-
const { id: organizationId } = await context.params
81-
82-
try {
83-
const body = await request.json()
84-
85-
if (typeof body.seats !== 'number' || body.seats < 1 || !Number.isInteger(body.seats)) {
86-
return badRequestResponse('seats must be a positive integer')
87-
}
88-
89-
const [orgData] = await db
90-
.select({ id: organization.id })
91-
.from(organization)
92-
.where(eq(organization.id, organizationId))
93-
.limit(1)
94-
95-
if (!orgData) {
96-
return notFoundResponse('Organization')
97-
}
98-
99-
const [subData] = await db
100-
.select()
101-
.from(subscription)
102-
.where(and(eq(subscription.referenceId, organizationId), eq(subscription.status, 'active')))
103-
.limit(1)
104-
105-
if (!subData) {
106-
return notFoundResponse('Subscription')
107-
}
108-
109-
const newSeatCount = body.seats
110-
let stripeUpdated = false
111-
112-
if (subData.plan === 'enterprise') {
113-
const currentMetadata = (subData.metadata as Record<string, unknown>) || {}
114-
const newMetadata = {
115-
...currentMetadata,
116-
seats: newSeatCount,
117-
}
118-
119-
await db
120-
.update(subscription)
121-
.set({ metadata: newMetadata })
122-
.where(eq(subscription.id, subData.id))
123-
124-
logger.info(`Admin API: Updated enterprise seats for organization ${organizationId}`, {
125-
seats: newSeatCount,
126-
})
127-
} else if (subData.plan === 'team') {
128-
if (subData.stripeSubscriptionId) {
129-
const stripe = requireStripeClient()
130-
131-
const stripeSubscription = await stripe.subscriptions.retrieve(subData.stripeSubscriptionId)
132-
133-
if (stripeSubscription.status !== 'active') {
134-
return badRequestResponse('Stripe subscription is not active')
135-
}
136-
137-
const subscriptionItem = stripeSubscription.items.data[0]
138-
if (!subscriptionItem) {
139-
return internalErrorResponse('No subscription item found in Stripe subscription')
140-
}
141-
142-
const currentSeats = subData.seats || 1
143-
144-
logger.info('Admin API: Updating Stripe subscription quantity', {
145-
organizationId,
146-
stripeSubscriptionId: subData.stripeSubscriptionId,
147-
subscriptionItemId: subscriptionItem.id,
148-
currentSeats,
149-
newSeatCount,
150-
})
151-
152-
await stripe.subscriptions.update(subData.stripeSubscriptionId, {
153-
items: [
154-
{
155-
id: subscriptionItem.id,
156-
quantity: newSeatCount,
157-
},
158-
],
159-
proration_behavior: 'create_prorations',
160-
})
161-
162-
stripeUpdated = true
163-
}
164-
165-
await db
166-
.update(subscription)
167-
.set({ seats: newSeatCount })
168-
.where(eq(subscription.id, subData.id))
169-
170-
logger.info(`Admin API: Updated team seats for organization ${organizationId}`, {
171-
seats: newSeatCount,
172-
stripeUpdated,
173-
})
174-
} else {
175-
await db
176-
.update(subscription)
177-
.set({ seats: newSeatCount })
178-
.where(eq(subscription.id, subData.id))
179-
180-
logger.info(`Admin API: Updated seats for organization ${organizationId}`, {
181-
seats: newSeatCount,
182-
plan: subData.plan,
183-
})
184-
}
185-
186-
return singleResponse({
187-
success: true,
188-
seats: newSeatCount,
189-
plan: subData.plan,
190-
stripeUpdated,
191-
})
192-
} catch (error) {
193-
logger.error('Admin API: Failed to update organization seats', { error, organizationId })
194-
return internalErrorResponse('Failed to update organization seats')
195-
}
196-
})

0 commit comments

Comments
 (0)