Merge branch 'dev' into promptless/document-payments-ledger-behavior
Some checks failed
DB migration compat / Check if migrations changed (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled

This commit is contained in:
promptless[bot] 2026-05-01 16:45:34 +00:00
commit bc563312d9
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,
});