import { AppEnv, FeatureUsageType, type FreeTrial, FreeTrialDuration, type ProductItem, type ProductV2, TierBehavior, } from "@autumn/shared"; import { items } from "@tests/utils/fixtures/items"; import { products } from "@tests/utils/fixtures/products"; import { clearOrg } from "@tests/utils/setup/clearOrg.js"; import defaultCtx, { createTestContext, type TestContext, } from "@tests/utils/testInitUtils/createTestContext.js"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; import { FeatureService } from "@/internal/features/FeatureService"; import { constructBooleanFeature, constructCreditSystem, constructMeteredFeature, } from "@/internal/features/utils/constructFeatureUtils"; import { constructFeatureItem, constructPrepaidItem, } from "@/utils/scriptUtils/constructItem"; import { buildRealisticCustomerSeed, createScenarioAutumn, seedCustomersWithEntities, } from "../seedUtils"; // ── Email platform seed config ── // Modeled loosely on Resend's pricing with deliberately tweaked numbers and // renamed feature flags so it is NOT an accurate mirror. Transactional plans have // NO base price: email volume is a prepaid item (volume-priced tiers) plus a // consumable overage. AI credits are a credit system. // Defaults for `bun scenario ep`; CLI flags override (e.g. `--count 1000`). const EP_SEED = { customerCount: 0, // 0 = catalog only (features + plans, no customers) concurrency: 10, attachPlan: null as "free" | "enterprise" | null, clear: true, // wipe org before seeding; --skip-clear keeps existing }; export const emailPlatformFeatureIds = { ai_actions: "ai_actions", ai_credits: "ai_credits", automation_runs: "automation_runs", contacts: "contacts", dedicated_ip: "dedicated_ip", domains: "domains", emails: "emails", engagement_tracking: "engagement_tracking", inbound_routing: "inbound_routing", multi_region: "multi_region", no_daily_limit: "no_daily_limit", priority_support: "priority_support", projects: "projects", scoped_api_keys: "scoped_api_keys", sending_receiving: "sending_receiving", slack_support: "slack_support", smtp_relay: "smtp_relay", soc2_reports: "soc2_reports", sso_saml: "sso_saml", ticket_support: "ticket_support", webhooks: "webhooks", } as const; export const emailPlatformPlanIds = { dedicatedIpPack: "dedicated_ip_pack", enterprise: "enterprise", free: "free", pro: "pro", proMarketing: "pro_marketing", scale: "scale", } as const; // Boolean flags shared by every plan ("all plans include ..."). const sharedBooleanFeatureIds = [ emailPlatformFeatureIds.sending_receiving, emailPlatformFeatureIds.smtp_relay, emailPlatformFeatureIds.inbound_routing, emailPlatformFeatureIds.engagement_tracking, emailPlatformFeatureIds.multi_region, emailPlatformFeatureIds.webhooks, emailPlatformFeatureIds.soc2_reports, emailPlatformFeatureIds.scoped_api_keys, ] as const; // Tier-specific boolean flags. const tieredBooleanFeatureIds = [ emailPlatformFeatureIds.ticket_support, emailPlatformFeatureIds.slack_support, emailPlatformFeatureIds.priority_support, emailPlatformFeatureIds.no_daily_limit, emailPlatformFeatureIds.dedicated_ip, emailPlatformFeatureIds.sso_saml, ] as const; export const emailPlatformBooleanFeatureIds = [ ...sharedBooleanFeatureIds, ...tieredBooleanFeatureIds, ] as const; type EmailPlatformFeatureId = (typeof emailPlatformFeatureIds)[keyof typeof emailPlatformFeatureIds]; const featureNames: Record = { ai_actions: "AI Actions", ai_credits: "AI Credits", automation_runs: "Automation Runs", contacts: "Contacts", dedicated_ip: "Dedicated IP", domains: "Domains", emails: "Emails", engagement_tracking: "Engagement Tracking", inbound_routing: "Inbound Routing", multi_region: "Multi-Region Sending", no_daily_limit: "No Daily Limit", priority_support: "Priority Support", projects: "Projects", scoped_api_keys: "Scoped API Keys", sending_receiving: "Sending & Receiving", slack_support: "Slack Support", smtp_relay: "SMTP Relay", soc2_reports: "SOC 2 Reports", sso_saml: "SSO / SAML", ticket_support: "Ticket Support", webhooks: "Webhooks", }; type EmailPlatformPlanMap = ReturnType< typeof buildEmailPlatformProducts >["plans"]; export type EmailPlatformPlanKey = keyof EmailPlatformPlanMap; const booleanItem = (featureId: EmailPlatformFeatureId): ProductItem => constructFeatureItem({ featureId, isBoolean: true, }) as ProductItem; const booleanItems = (featureIds: readonly EmailPlatformFeatureId[]) => featureIds.map(booleanItem); type VolumeTier = { amount: number; to: number | "inf"; flat_amount: number }; // Prepaid email volume (whole purchased quantity billed at the tier flat amount) // plus a per-1k consumable overage for sends beyond the purchased volume. const emailItems = ({ tiers, overagePerThousand, }: { tiers: VolumeTier[]; overagePerThousand: number; }) => [ constructPrepaidItem({ featureId: emailPlatformFeatureIds.emails, billingUnits: 1, includedUsage: 0, tierBehaviour: TierBehavior.VolumeBased, tiers, }) as ProductItem, items.consumable({ featureId: emailPlatformFeatureIds.emails, billingUnits: 1_000, price: overagePerThousand, }), ]; // 10k automation runs included, then per-run overage. const automationItems = () => [ items.consumable({ featureId: emailPlatformFeatureIds.automation_runs, includedUsage: 10_000, billingUnits: 1, price: 0.0015, }), ]; const proEmailTiers: VolumeTier[] = [ { amount: 0, to: 50_000, flat_amount: 20 }, { amount: 0, to: 100_000, flat_amount: 35 }, { amount: 0, to: "inf", flat_amount: 60 }, ]; const scaleEmailTiers: VolumeTier[] = [ { amount: 0, to: 100_000, flat_amount: 90 }, { amount: 0, to: 200_000, flat_amount: 160 }, { amount: 0, to: 500_000, flat_amount: 350 }, { amount: 0, to: 1_000_000, flat_amount: 650 }, { amount: 0, to: 1_500_000, flat_amount: 825 }, { amount: 0, to: 2_500_000, flat_amount: 1_150 }, { amount: 0, to: "inf", flat_amount: 1_500 }, ]; // Marketing is capped by contacts, not emails: prepaid contact tiers, no overage. const marketingContactTiers: VolumeTier[] = [ { amount: 0, to: 5_000, flat_amount: 40 }, { amount: 0, to: 10_000, flat_amount: 80 }, { amount: 0, to: 15_000, flat_amount: 120 }, { amount: 0, to: 25_000, flat_amount: 180 }, { amount: 0, to: 50_000, flat_amount: 250 }, { amount: 0, to: 100_000, flat_amount: 450 }, { amount: 0, to: 150_000, flat_amount: 650 }, { amount: 0, to: "inf", flat_amount: 900 }, ]; export const buildEmailPlatformFeatures = ({ ctx }: { ctx: TestContext }) => { const f = emailPlatformFeatureIds; const orgId = ctx.org.id; const env = ctx.env; return [ constructMeteredFeature({ featureId: f.emails, name: featureNames[f.emails], orgId, env, usageType: FeatureUsageType.Single, eventNames: ["emails"], }), constructMeteredFeature({ featureId: f.automation_runs, name: featureNames[f.automation_runs], orgId, env, usageType: FeatureUsageType.Single, eventNames: ["automation_runs"], }), constructMeteredFeature({ featureId: f.ai_actions, name: featureNames[f.ai_actions], orgId, env, usageType: FeatureUsageType.Single, eventNames: ["ai_actions"], }), constructCreditSystem({ featureId: f.ai_credits, orgId, env, schema: [{ metered_feature_id: f.ai_actions, credit_cost: 1 }], }), constructMeteredFeature({ featureId: f.contacts, name: featureNames[f.contacts], orgId, env, usageType: FeatureUsageType.Continuous, }), constructMeteredFeature({ featureId: f.domains, name: featureNames[f.domains], orgId, env, usageType: FeatureUsageType.Continuous, }), constructMeteredFeature({ featureId: f.projects, name: featureNames[f.projects], orgId, env, usageType: FeatureUsageType.Continuous, }), ...emailPlatformBooleanFeatureIds.map((featureId) => constructBooleanFeature({ featureId, name: featureNames[featureId], orgId, env, }), ), ]; }; export const ensureEmailPlatformFeatures = async ({ ctx = defaultCtx, }: { ctx?: TestContext; } = {}) => { const desiredFeatures = buildEmailPlatformFeatures({ ctx }); const existingFeatures = await FeatureService.list({ db: ctx.db, orgId: ctx.org.id, env: ctx.env, }); const existingById = new Map( existingFeatures.map((feature) => [feature.id, feature]), ); const featuresToInsert = desiredFeatures.filter( (feature) => !existingById.has(feature.id), ); const featuresToUpdate = desiredFeatures.filter((feature) => existingById.has(feature.id), ); if (featuresToInsert.length > 0) { await FeatureService.insert({ db: ctx.db, data: featuresToInsert, logger: console, }); } await Promise.all( featuresToUpdate.map((feature) => FeatureService.update({ db: ctx.db, id: feature.id, orgId: ctx.org.id, env: ctx.env, updates: { name: feature.name, type: feature.type, config: feature.config, event_names: feature.event_names, model_markups: feature.model_markups, archived: false, }, }), ), ); ctx.features = await FeatureService.list({ db: ctx.db, orgId: ctx.org.id, env: ctx.env, }); return ctx.features.filter((feature) => desiredFeatures.some((desired) => desired.id === feature.id), ); }; export const buildEmailPlatformProducts = () => { const f = emailPlatformFeatureIds; const p = emailPlatformPlanIds; const freeBooleans = [...sharedBooleanFeatureIds, f.ticket_support] as const; const proBooleans = [ ...sharedBooleanFeatureIds, f.ticket_support, f.no_daily_limit, ] as const; const scaleBooleans = [ ...sharedBooleanFeatureIds, f.slack_support, f.no_daily_limit, ] as const; const enterpriseBooleans = [ ...sharedBooleanFeatureIds, f.priority_support, f.no_daily_limit, f.sso_saml, f.dedicated_ip, ] as const; const plans = { // Free: fixed allowances, daily-limited, no overage on automations. free: products.base({ id: p.free, items: [ items.free({ featureId: f.emails, includedUsage: 3_000 }), items.free({ featureId: f.automation_runs, includedUsage: 10_000 }), items.free({ featureId: f.ai_credits, includedUsage: 5 }), items.free({ featureId: f.domains, includedUsage: 1 }), ...booleanItems(freeBooleans), ], }), // Pro/Scale: NO base price — email volume is the prepaid item. pro: products.base({ id: p.pro, items: [ ...emailItems({ tiers: proEmailTiers, overagePerThousand: 0.9 }), ...automationItems(), items.free({ featureId: f.ai_credits, includedUsage: 100 }), items.free({ featureId: f.domains, includedUsage: 10 }), ...booleanItems(proBooleans), ], }), scale: products.base({ id: p.scale, items: [ ...emailItems({ tiers: scaleEmailTiers, overagePerThousand: 0.5 }), ...automationItems(), items.free({ featureId: f.ai_credits, includedUsage: 500 }), items.free({ featureId: f.domains, includedUsage: 1_000 }), ...booleanItems(scaleBooleans), ], }), // Marketing: capped by contacts (prepaid tiers), not email volume. proMarketing: products.base({ id: p.proMarketing, items: [ constructPrepaidItem({ featureId: f.contacts, billingUnits: 1, includedUsage: 0, tierBehaviour: TierBehavior.VolumeBased, tiers: marketingContactTiers, }) as ProductItem, items.free({ featureId: f.ai_credits, includedUsage: 100 }), ...booleanItems([...sharedBooleanFeatureIds, f.no_daily_limit]), ], }), // Enterprise: custom, no base price, flexible everything. enterprise: products.base({ id: p.enterprise, items: [ items.free({ featureId: f.emails, includedUsage: 3_000_000 }), ...automationItems(), items.free({ featureId: f.ai_credits, includedUsage: 5_000 }), items.free({ featureId: f.domains, includedUsage: 5_000 }), items.free({ featureId: f.contacts, includedUsage: 150_000 }), ...booleanItems(enterpriseBooleans), ], }), dedicatedIpPack: products.base({ id: p.dedicatedIpPack, isAddOn: true, items: [items.monthlyPrice({ price: 30 }), booleanItem(f.dedicated_ip)], }), } satisfies Record; return { featureIds: emailPlatformFeatureIds, planIds: emailPlatformPlanIds, proBooleans, scaleBooleans, enterpriseBooleans, plans, }; }; const withProductGroup = ({ products, group, }: { products: ProductV2[]; group: string; }) => products.map((product) => ({ ...product, group, })); export const initEmailPlatformScenario = async ({ customerId = "agent-email-platform", attachPlan = "free", entityCount = 2, paymentMethod = "success", ctx = defaultCtx, }: { customerId?: string; attachPlan?: EmailPlatformPlanKey | null; entityCount?: number; paymentMethod?: "success" | "fail" | "authenticate" | "alipay"; ctx?: TestContext; } = {}) => { await ensureEmailPlatformFeatures({ ctx }); const catalog = buildEmailPlatformProducts(); const setup = [ s.customer({ paymentMethod }), s.products({ list: Object.values(catalog.plans) }), ...(entityCount > 0 ? [ s.entities({ count: entityCount, featureId: catalog.featureIds.projects, }), ] : []), ]; const actions = attachPlan ? [s.billing.attach({ productId: catalog.plans[attachPlan].id })] : []; const scenario = await initScenario({ customerId, setup, actions, ctx, }); return { ...scenario, ...catalog, }; }; /** Return a copy of a plan with a card-required free trial, for trialing-state scenarios. */ export const withFreeTrial = ({ product, trialDays, }: { product: ProductV2; trialDays: number; }): ProductV2 => ({ ...product, free_trial: { length: trialDays, duration: FreeTrialDuration.Day, unique_fingerprint: false, card_required: true, } as unknown as FreeTrial, }); export const seedEmailPlatformCustomers = async ({ customerCount = 1_000, idPrefix = "ep-customer", entityCountForCustomer, productPrefix = "email-platform", attachPlan = null, concurrency = 10, deleteExisting = true, ctx = defaultCtx, }: { customerCount?: number; idPrefix?: string; entityCountForCustomer?: (index: number) => 1 | 2; productPrefix?: string; attachPlan?: Extract | null; concurrency?: number; deleteExisting?: boolean; ctx?: TestContext; } = {}) => { await ensureEmailPlatformFeatures({ ctx }); const catalog = buildEmailPlatformProducts(); await initScenario({ setup: [ s.products({ list: withProductGroup({ products: Object.values(catalog.plans), group: productPrefix, }), prefix: "", createInStripe: false, }), ], actions: [], ctx, }); const customers = Array.from({ length: customerCount }, (_, index) => { return { ...buildRealisticCustomerSeed({ index, idPrefix, entityFeatureId: emailPlatformFeatureIds.projects, entityCount: entityCountForCustomer?.(index), }), attachPlanId: attachPlan ? catalog.plans[attachPlan].id : null, }; }); const seeded = await seedCustomersWithEntities({ autumn: createScenarioAutumn({ ctx }), customers, concurrency, deleteExisting, }); return { ...catalog, productPrefix, ...seeded, }; }; const getArgValue = (name: string) => { const prefix = `${name}=`; const inline = process.argv.find((arg) => arg.startsWith(prefix)); if (inline) return inline.slice(prefix.length); const index = process.argv.indexOf(name); return index === -1 ? undefined : process.argv[index + 1]; }; export const runEmailPlatformSeed = async () => { const customerCount = Number(getArgValue("--count") ?? EP_SEED.customerCount); const concurrency = Number( getArgValue("--concurrency") ?? EP_SEED.concurrency, ); const attachPlan = (getArgValue("--attach-plan") ?? EP_SEED.attachPlan) as | Extract | null; if (!process.env.TESTS_ORG) { throw new Error("TESTS_ORG is required to seed email platform data"); } if (EP_SEED.clear && !process.argv.includes("--skip-clear")) { await clearOrg({ orgSlug: process.env.TESTS_ORG, env: AppEnv.Sandbox, skipStripeReset: true, }); } const ctx = await createTestContext(); const result = await seedEmailPlatformCustomers({ ctx, customerCount, concurrency, attachPlan: attachPlan ?? null, deleteExisting: !process.argv.includes("--keep-existing"), }); console.log("Email platform seed complete", { customers: result.customerCount, entities: result.entityCount, productPrefix: result.productPrefix, attachPlan: attachPlan ?? null, }); }; if (import.meta.main) { runEmailPlatformSeed() .catch((error) => { console.error("Email platform seed failed:", error); process.exit(1); }) .finally(() => { process.exit(0); }); }