11/* eslint-disable no-restricted-syntax */
22import { usersCrudHandlers } from '@/app/api/latest/users/crud' ;
3+ import { CustomerType , Prisma , PurchaseCreationSource , SubscriptionStatus } from '@/generated/prisma/client' ;
34import { overrideBranchConfigOverride } from '@/lib/config' ;
45import {
56 LOCAL_EMULATOR_ADMIN_EMAIL ,
@@ -15,9 +16,12 @@ import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from '@/lib/tenanc
1516import { getPrismaClientForTenancy , globalPrismaClient } from '@/prisma-client' ;
1617import { ALL_APPS } from '@stackframe/stack-shared/dist/apps/apps-config' ;
1718import { ITEM_IDS , PLAN_LIMITS } from '@stackframe/stack-shared/dist/plans' ;
19+ import { DayInterval } from '@stackframe/stack-shared/dist/utils/dates' ;
1820import { throwErr } from '@stackframe/stack-shared/dist/utils/errors' ;
1921import { typedEntries , typedFromEntries } from '@stackframe/stack-shared/dist/utils/objects' ;
2022
23+ const MONTHLY_REPEAT : DayInterval = [ 1 , "month" ] ;
24+
2125const DUMMY_PROJECT_ID = '6fbbf22e-f4b2-4c6e-95a1-beab6fa41063' ;
2226
2327let didEnableSeedLogTimestamps = false ;
@@ -159,9 +163,10 @@ export async function seed() {
159163 includedItems : {
160164 [ ITEM_IDS . seats ] : { quantity : PLAN_LIMITS . free . seats , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
161165 [ ITEM_IDS . authUsers ] : { quantity : PLAN_LIMITS . free . authUsers , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
162- [ ITEM_IDS . emailsPerMonth ] : { quantity : PLAN_LIMITS . free . emailsPerMonth , repeat : "never" as const , expires : "when-purchase-expires " as const } ,
166+ [ ITEM_IDS . emailsPerMonth ] : { quantity : PLAN_LIMITS . free . emailsPerMonth , repeat : MONTHLY_REPEAT , expires : "when-repeated " as const } ,
163167 [ ITEM_IDS . analyticsTimeoutSeconds ] : { quantity : PLAN_LIMITS . free . analyticsTimeoutSeconds , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
164- [ ITEM_IDS . analyticsEvents ] : { quantity : PLAN_LIMITS . free . analyticsEvents , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
168+ [ ITEM_IDS . analyticsEvents ] : { quantity : PLAN_LIMITS . free . analyticsEvents , repeat : MONTHLY_REPEAT , expires : "when-repeated" as const } ,
169+ [ ITEM_IDS . sessionReplays ] : { quantity : PLAN_LIMITS . free . sessionReplays , repeat : MONTHLY_REPEAT , expires : "when-repeated" as const } ,
165170 } ,
166171 } ,
167172 team : {
@@ -173,16 +178,18 @@ export async function seed() {
173178 prices : {
174179 monthly : {
175180 USD : "49" ,
176- interval : [ 1 , "month" ] as any ,
181+ interval : MONTHLY_REPEAT ,
177182 serverOnly : false ,
178183 } ,
179184 } ,
180185 includedItems : {
181186 [ ITEM_IDS . seats ] : { quantity : PLAN_LIMITS . team . seats , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
182187 [ ITEM_IDS . authUsers ] : { quantity : PLAN_LIMITS . team . authUsers , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
183- [ ITEM_IDS . emailsPerMonth ] : { quantity : PLAN_LIMITS . team . emailsPerMonth , repeat : "never" as const , expires : "when-purchase-expires " as const } ,
188+ [ ITEM_IDS . emailsPerMonth ] : { quantity : PLAN_LIMITS . team . emailsPerMonth , repeat : MONTHLY_REPEAT , expires : "when-repeated " as const } ,
184189 [ ITEM_IDS . analyticsTimeoutSeconds ] : { quantity : PLAN_LIMITS . team . analyticsTimeoutSeconds , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
185- [ ITEM_IDS . analyticsEvents ] : { quantity : PLAN_LIMITS . team . analyticsEvents , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
190+ [ ITEM_IDS . analyticsEvents ] : { quantity : PLAN_LIMITS . team . analyticsEvents , repeat : MONTHLY_REPEAT , expires : "when-repeated" as const } ,
191+ [ ITEM_IDS . sessionReplays ] : { quantity : PLAN_LIMITS . team . sessionReplays , repeat : MONTHLY_REPEAT , expires : "when-repeated" as const } ,
192+ [ ITEM_IDS . onboardingCall ] : { quantity : 1 , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
186193 } ,
187194 } ,
188195 growth : {
@@ -194,16 +201,18 @@ export async function seed() {
194201 prices : {
195202 monthly : {
196203 USD : "299" ,
197- interval : [ 1 , "month" ] as any ,
204+ interval : MONTHLY_REPEAT ,
198205 serverOnly : false ,
199206 } ,
200207 } ,
201208 includedItems : {
202209 [ ITEM_IDS . seats ] : { quantity : PLAN_LIMITS . growth . seats , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
203210 [ ITEM_IDS . authUsers ] : { quantity : PLAN_LIMITS . growth . authUsers , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
204- [ ITEM_IDS . emailsPerMonth ] : { quantity : PLAN_LIMITS . growth . emailsPerMonth , repeat : "never" as const , expires : "when-purchase-expires " as const } ,
211+ [ ITEM_IDS . emailsPerMonth ] : { quantity : PLAN_LIMITS . growth . emailsPerMonth , repeat : MONTHLY_REPEAT , expires : "when-repeated " as const } ,
205212 [ ITEM_IDS . analyticsTimeoutSeconds ] : { quantity : PLAN_LIMITS . growth . analyticsTimeoutSeconds , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
206- [ ITEM_IDS . analyticsEvents ] : { quantity : PLAN_LIMITS . growth . analyticsEvents , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
213+ [ ITEM_IDS . analyticsEvents ] : { quantity : PLAN_LIMITS . growth . analyticsEvents , repeat : MONTHLY_REPEAT , expires : "when-repeated" as const } ,
214+ [ ITEM_IDS . sessionReplays ] : { quantity : PLAN_LIMITS . growth . sessionReplays , repeat : MONTHLY_REPEAT , expires : "when-repeated" as const } ,
215+ [ ITEM_IDS . onboardingCall ] : { quantity : 1 , repeat : "never" as const , expires : "when-purchase-expires" as const } ,
207216 } ,
208217 } ,
209218 "extra-seats" : {
@@ -215,7 +224,7 @@ export async function seed() {
215224 prices : {
216225 monthly : {
217226 USD : "29" ,
218- interval : [ 1 , "month" ] as any ,
227+ interval : MONTHLY_REPEAT ,
219228 serverOnly : false ,
220229 } ,
221230 } ,
@@ -234,6 +243,8 @@ export async function seed() {
234243 [ ITEM_IDS . emailsPerMonth ] : { displayName : "Emails per Month" , customerType : "team" as const } ,
235244 [ ITEM_IDS . analyticsTimeoutSeconds ] : { displayName : "Analytics Timeout (seconds)" , customerType : "team" as const } ,
236245 [ ITEM_IDS . analyticsEvents ] : { displayName : "Analytics Events" , customerType : "team" as const } ,
246+ [ ITEM_IDS . sessionReplays ] : { displayName : "Session Replays" , customerType : "team" as const } ,
247+ [ ITEM_IDS . onboardingCall ] : { displayName : "Onboarding Call" , customerType : "team" as const } ,
237248 } ,
238249 } ,
239250 apps : {
@@ -292,6 +303,60 @@ export async function seed() {
292303 console . log ( 'Internal team created' ) ;
293304 }
294305
306+ // The team-create CRUD path auto-grants the free plan to every team in the
307+ // internal project, but the internal team itself is written directly above
308+ // (bypassing that code path), so it would otherwise end up with zero
309+ // entitlements and trip the plan-limit enforcement. Grant it the Growth plan
310+ // so Stack Auth employees using the dashboard get full quotas. Idempotent —
311+ // skipped if an active Growth subscription already exists.
312+ //
313+ // We create the subscription with raw Prisma (matching seed-dummy-data.ts)
314+ // rather than grantProductToCustomer because bulldozer storage tables
315+ // aren't initialized at this point in the seed yet. The Bulldozer init
316+ // call right below this block ingresses the row into the ledger.
317+ const growthProduct = updatedInternalTenancy . config . payments . products . growth ;
318+ if ( growthProduct . customerType === 'team' ) {
319+ const existingGrowthSub = await internalPrisma . subscription . findFirst ( {
320+ where : {
321+ tenancyId : internalTenancy . id ,
322+ customerId : internalTeamId ,
323+ customerType : CustomerType . TEAM ,
324+ productId : 'growth' ,
325+ status : SubscriptionStatus . active ,
326+ } ,
327+ } ) ;
328+ if ( ! existingGrowthSub ) {
329+ const growthPrices = growthProduct . prices === 'include-by-default' ? { } : growthProduct . prices ;
330+ const firstPriceId = Object . keys ( growthPrices ) [ 0 ] ?? null ;
331+ const now = new Date ( ) ;
332+ // Clone to ensure the stored JSON snapshot is independent of the config object
333+ // (mirrors the pattern used in seed-dummy-data.ts).
334+ const storedProduct = JSON . parse ( JSON . stringify ( growthProduct ) ) as Prisma . InputJsonValue ;
335+ // Mirror what a real Stripe checkout would produce, based on whether
336+ // the internal project is running in test mode.
337+ const creationSource = updatedInternalTenancy . config . payments . testMode
338+ ? PurchaseCreationSource . TEST_MODE
339+ : PurchaseCreationSource . PURCHASE_PAGE ;
340+ await internalPrisma . subscription . create ( {
341+ data : {
342+ tenancyId : internalTenancy . id ,
343+ customerId : internalTeamId ,
344+ customerType : CustomerType . TEAM ,
345+ status : SubscriptionStatus . active ,
346+ productId : 'growth' ,
347+ priceId : firstPriceId ,
348+ product : storedProduct ,
349+ quantity : 1 ,
350+ currentPeriodStart : now ,
351+ currentPeriodEnd : new Date ( '2099-12-31T23:59:59Z' ) ,
352+ cancelAtPeriodEnd : false ,
353+ creationSource,
354+ } ,
355+ } ) ;
356+ console . log ( 'Granted Growth plan to internal team' ) ;
357+ }
358+ }
359+
295360 // Upsert the internal API key set before any flake-prone work (dummy-project
296361 // seed, email/svix, clickhouse). The emulator CLI authenticates against the
297362 // internal project using the pck stored here, so it must land before the rest
0 commit comments