-
Notifications
You must be signed in to change notification settings - Fork 3.5k
improvement(billing): route scope by subscription referenceId, sync plan from Stripe, transfer storage on org join, outbox service #4219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
3f6142e
2a4a417
c1e39ac
2bb951d
62727f0
e561c3b
02161e1
d2049d2
420e1df
1e0e5e4
fca19e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,15 +14,15 @@ import { | |
| workspaceInvitation, | ||
| } from '@sim/db/schema' | ||
| import { createLogger } from '@sim/logger' | ||
| import { and, eq, inArray } from 'drizzle-orm' | ||
| import { and, eq, inArray, sql } from 'drizzle-orm' | ||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { z } from 'zod' | ||
| import { getEmailSubject, renderInvitationEmail } from '@/components/emails' | ||
| import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' | ||
| import { getSession } from '@/lib/auth' | ||
| import { hasAccessControlAccess } from '@/lib/billing' | ||
| import { syncUsageLimitsFromSubscription } from '@/lib/billing/core/usage' | ||
| import { isOrgPlan, sqlIsPro } from '@/lib/billing/plan-helpers' | ||
| import { isPaid, sqlIsPro } from '@/lib/billing/plan-helpers' | ||
| import { requireStripeClient } from '@/lib/billing/stripe-client' | ||
| import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' | ||
| import { getBaseUrl } from '@/lib/core/utils/urls' | ||
|
|
@@ -356,7 +356,10 @@ export async function PUT( | |
| .limit(1) | ||
|
|
||
| const orgSub = orgSubs[0] | ||
| const orgIsPaid = orgSub && isOrgPlan(orgSub.plan) | ||
| // Any paid subscription attached to the org triggers the | ||
| // "snapshot my personal Pro usage and cancel it" flow — includes | ||
| // `pro_*` plans transferred to the org, not just team/enterprise. | ||
| const orgIsPaid = orgSub && isPaid(orgSub.plan) | ||
|
|
||
| if (orgIsPaid) { | ||
| const userId = session.user.id | ||
|
|
@@ -410,6 +413,51 @@ export async function PUT( | |
| personalProToCancel = personalPro | ||
| } | ||
| } | ||
|
|
||
| // Transfer the joining user's accumulated personal storage | ||
| // bytes into the organization's pool. After this point | ||
| // `isOrgScopedSubscription` returns true for the user, so | ||
| // `getUserStorageUsage`/`incrementStorageUsage`/`decrementStorageUsage` | ||
| // all route through `organization.storageUsedBytes`. Without | ||
| // this transfer, pre-join bytes would be orphaned on the | ||
| // user's row and subsequent decrements (deleting a pre-join | ||
| // file after joining) would wrongly reduce the org pool. | ||
| // | ||
| // `.for('update')` acquires a row-level write lock on the | ||
| // user's `user_stats` row so a concurrent | ||
| // `incrementStorageUsage`/`decrementStorageUsage` (from | ||
| // another tab, a scheduled run, an API-key writer, etc.) | ||
| // blocks until this transaction commits — otherwise Postgres | ||
| // READ COMMITTED would let a write land between the snapshot | ||
| // SELECT and the zero UPDATE, silently dropping those bytes. | ||
| // Mirrors the bulk version in `syncSubscriptionUsageLimits`. | ||
| const storageRows = await tx | ||
| .select({ storageUsedBytes: userStats.storageUsedBytes }) | ||
| .from(userStats) | ||
| .where(eq(userStats.userId, userId)) | ||
| .for('update') | ||
| .limit(1) | ||
|
|
||
| const bytesToTransfer = storageRows[0]?.storageUsedBytes ?? 0 | ||
| if (bytesToTransfer > 0) { | ||
| await tx | ||
| .update(organization) | ||
| .set({ | ||
| storageUsedBytes: sql`${organization.storageUsedBytes} + ${bytesToTransfer}`, | ||
| }) | ||
| .where(eq(organization.id, organizationId)) | ||
|
|
||
| await tx | ||
| .update(userStats) | ||
| .set({ storageUsedBytes: 0 }) | ||
| .where(eq(userStats.userId, userId)) | ||
|
|
||
| logger.info('Transferred personal storage bytes to org pool on join', { | ||
| userId, | ||
| organizationId, | ||
| bytes: bytesToTransfer, | ||
| }) | ||
| } | ||
|
icecrasher321 marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Storage transfer runs even without personal Pro subscriptionLow Severity The storage transfer block (lines 421–447) is inside the Reviewed by Cursor Bugbot for commit fca19e8. Configure here.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this didn't exist before, no regression |
||
| } | ||
| } catch (error) { | ||
| logger.error('Failed to handle Pro user joining team', { | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.