Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/CLAUDE-KNOWLEDGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`.
23 changes: 23 additions & 0 deletions apps/backend/src/lib/seed-dummy-data.test.ts
Original file line number Diff line number Diff line change
@@ -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",
}
`);
});
});
43 changes: 22 additions & 21 deletions apps/backend/src/lib/seed-dummy-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Comment thread
mantrakp04 marked this conversation as resolved.
stackable: false,
freeTrial: twoWeekInterval as any,
Expand Down Expand Up @@ -744,7 +748,7 @@ function buildDummyPaymentsSetup(): PaymentsSetup {
'growth': {
displayName: 'Growth',
productLineId: 'workspace',
customerType: 'user',
customerType: 'team',
Comment thread
mantrakp04 marked this conversation as resolved.
serverOnly: false,
stackable: false,
prices: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -2094,7 +2096,6 @@ export async function seedDummyProject(options: SeedDummyProjectOptions): Promis
prisma: dummyPrisma,
tenancyId: dummyTenancy.id,
teamNameToId,
userEmailToId,
paymentsProducts,
});

Expand Down
Loading