[codex] Fix preview dummy payments customer types (#1398)

## Summary

Fixes preview dummy payments seed data so seeded products and items
match their team-scoped product lines.

## Root Cause

The preview seed configured `workspace` and `add_ons` product lines with
`customerType: "team"`, but the products inside those lines (`starter`,
`growth`, and `regression-addon`) were configured as `customerType:
"user"`. Environment override writes validate against the rendered
branch config, so unrelated environment updates could fail with a
product/product-line customer type warning.

## Changes

- Mark preview dummy payments products and included items as
team-scoped.
- Export the dummy payments setup helper for focused validation.
- Add a regression test that validates the generated branch payments
override has no config override errors or incomplete config warnings.

## Validation

Passed in the original checkout with dependencies installed:

- `STACK_SKIP_TEMPLATE_GENERATION=true pnpm exec vitest run --config
vitest.config.ts src/lib/seed-dummy-data.test.ts --reporter=verbose
--maxWorkers=1 --minWorkers=1`
- `pnpm -C apps/backend lint src/lib/seed-dummy-data.ts
src/lib/seed-dummy-data.test.ts`
- `pnpm -C apps/backend typecheck`

The temporary clean worktree used for this PR did not have
`node_modules`, so dependency-backed commands were not rerun there.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Strengthened payment product configuration with tighter typing and
validation
* Normalized product customer types (switched relevant dummy data from
user to team) for consistency

* **Tests**
* Added tests validating dummy payments configuration and
branch/override validation

* **Documentation**
* Added Q&A documenting a configuration validation failure mode and
required consistency for dummy payments data
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Mantra 2026-05-01 09:44:30 -07:00 committed by GitHub
parent e831972c4c
commit d2f2fb0e42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 48 additions and 21 deletions

View File

@ -361,3 +361,6 @@ A: Invalid `tools` entries are rejected by `requestBodySchema` in `apps/backend/
## Q: Why did the internal metrics E2E snapshots need to change in April 2026?
A: The `/api/v1/internal/metrics` response now intentionally includes `analytics_overview.daily_anonymous_visitors_fallback`, `analytics_overview.anonymous_visitors_fallback`, and `active_users_by_country`. Those additions are reflected in `packages/stack-shared/src/interface/admin-metrics.ts` and the backend route, so the E2E snapshots must include them instead of treating them as regressions.
## Q: Why can environment config override writes fail with a product/product-line customer type warning after creating a preview project?
A: The environment override endpoint validates the new environment override against the rendered branch config. Preview dummy payments data must therefore be internally coherent: products assigned to a product line need the same `customerType` as that product line, otherwise unrelated environment patches can fail with warnings like `Product "growth" has customer type "user" but its product line "workspace" has customer type "team"`.

View File

@ -0,0 +1,23 @@
import { branchConfigSchema, getConfigOverrideErrors, getIncompleteConfigWarnings } from "@stackframe/stack-shared/dist/config/schema";
import { describe, expect, it } from "vitest";
import { buildDummyPaymentsSetup } from "./seed-dummy-data";
describe("dummy payments seed config", () => {
it("is valid branch payments config", async () => {
const { paymentsBranchOverride } = buildDummyPaymentsSetup();
const branchConfigOverride = { payments: paymentsBranchOverride };
expect(await getConfigOverrideErrors(branchConfigSchema, branchConfigOverride)).toMatchInlineSnapshot(`
{
"data": null,
"status": "ok",
}
`);
expect(await getIncompleteConfigWarnings(branchConfigSchema, branchConfigOverride)).toMatchInlineSnapshot(`
{
"data": null,
"status": "ok",
}
`);
});
});

View File

@ -11,6 +11,7 @@ import { getPrismaClientForTenancy, globalPrismaClient, type PrismaClientTransac
import { ALL_APPS } from '@stackframe/stack-shared/dist/apps/apps-config';
import { DEFAULT_EMAIL_THEME_ID } from '@stackframe/stack-shared/dist/helpers/emails';
import { type AdminUserProjectsCrud, type ProjectsCrud } from '@stackframe/stack-shared/dist/interface/crud/projects';
import { type Config } from '@stackframe/stack-shared/dist/config/format';
import { DayInterval } from '@stackframe/stack-shared/dist/utils/dates';
import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env';
import { throwErr } from '@stackframe/stack-shared/dist/utils/errors';
@ -94,18 +95,21 @@ type SeedDummyUsersOptions = {
teamNameToId: Map<string, string>,
};
type PaymentsProducts = {
[productId: string]: Config | undefined,
};
type PaymentsSetup = {
paymentsProducts: Record<string, unknown>,
paymentsBranchOverride: Record<string, unknown>,
paymentsEnvironmentOverride: Record<string, unknown>,
paymentsProducts: PaymentsProducts,
paymentsBranchOverride: Config,
paymentsEnvironmentOverride: Config,
};
type TransactionsSeedOptions = {
prisma: PrismaClientTransaction,
tenancyId: string,
teamNameToId: Map<string, string>,
userEmailToId: Map<string, string>,
paymentsProducts: Record<string, unknown>,
paymentsProducts: PaymentsProducts,
};
type EmailSeedOptions = {
@ -707,16 +711,16 @@ async function seedDummyUsers(options: SeedDummyUsersOptions): Promise<Map<strin
return userEmailToId;
}
function buildDummyPaymentsSetup(): PaymentsSetup {
export function buildDummyPaymentsSetup(): PaymentsSetup {
const monthlyInterval: DayInterval = [1, 'month'];
const yearlyInterval: DayInterval = [1, 'year'];
const twoWeekInterval: DayInterval = [2, 'week'];
const paymentsProducts: Record<string, unknown> = {
const paymentsProducts: PaymentsProducts = {
'starter': {
displayName: 'Starter',
productLineId: 'workspace',
customerType: 'user',
customerType: 'team',
serverOnly: false,
stackable: false,
freeTrial: twoWeekInterval as any,
@ -744,7 +748,7 @@ function buildDummyPaymentsSetup(): PaymentsSetup {
'growth': {
displayName: 'Growth',
productLineId: 'workspace',
customerType: 'user',
customerType: 'team',
serverOnly: false,
stackable: false,
prices: {
@ -780,7 +784,7 @@ function buildDummyPaymentsSetup(): PaymentsSetup {
'regression-addon': {
displayName: 'Regression Add-on',
productLineId: 'add_ons',
customerType: 'user',
customerType: 'team',
serverOnly: false,
stackable: true,
prices: {
@ -818,19 +822,19 @@ function buildDummyPaymentsSetup(): PaymentsSetup {
items: {
studio_seats: {
displayName: 'Studio Seats',
customerType: 'user',
customerType: 'team',
},
review_passes: {
displayName: 'Reviewer Passes',
customerType: 'user',
customerType: 'team',
},
automation_minutes: {
displayName: 'Automation Minutes',
customerType: 'user',
customerType: 'team',
},
snapshot_credits: {
displayName: 'Snapshot Credits',
customerType: 'user',
customerType: 'team',
},
},
products: paymentsProducts,
@ -895,12 +899,10 @@ async function seedDummyTransactions(options: TransactionsSeedOptions) {
prisma,
tenancyId,
teamNameToId,
userEmailToId,
paymentsProducts,
} = options;
const resolveTeamId = (teamName: string) => teamNameToId.get(teamName) ?? throwErr(`Unknown dummy project team ${teamName}`);
const resolveUserId = (email: string) => userEmailToId.get(email) ?? throwErr(`Unknown dummy project user ${email}`);
const resolveProduct = (productId: string): Prisma.InputJsonValue => {
const product = paymentsProducts[productId];
if (!product) {
@ -944,8 +946,8 @@ async function seedDummyTransactions(options: TransactionsSeedOptions) {
},
{
id: DUMMY_SEED_IDS.subscriptions.mateoGrowthAnnual,
customerType: CustomerType.USER,
customerId: resolveUserId('mateo.silva@dummy.dev'),
customerType: CustomerType.TEAM,
customerId: resolveTeamId('Growth Loop'),
productId: 'growth',
priceId: 'annual',
product: resolveProduct('growth'),
@ -1089,8 +1091,8 @@ async function seedDummyTransactions(options: TransactionsSeedOptions) {
const oneTimePurchaseSeeds: OneTimePurchaseSeed[] = [
{
id: DUMMY_SEED_IDS.oneTimePurchases.ameliaSeatPack,
customerType: CustomerType.USER,
customerId: resolveUserId('amelia.chen@dummy.dev'),
customerType: CustomerType.TEAM,
customerId: resolveTeamId('Design Systems Lab'),
productId: 'starter',
priceId: 'monthly',
product: resolveProduct('starter'),
@ -2094,7 +2096,6 @@ export async function seedDummyProject(options: SeedDummyProjectOptions): Promis
prisma: dummyPrisma,
tenancyId: dummyTenancy.id,
teamNameToId,
userEmailToId,
paymentsProducts,
});