add diffPlanV1 diff/apply test suite with real customer fixtures

Round-trip tests against Firecrawl, Runable, RevisionDojo, and OnePREP
plan data proving diffPlanV1 + applyDiff reconstructs variants correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ayush Rodrigues
2026-05-29 13:12:12 +01:00
committed by Charlie Lamb
parent aa8f887e11
commit a8e22d05a9
17 changed files with 13689 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import { type ApiPlanV1 } from "@autumn/shared";
import { findById } from "./utils/findById.js";
// Bun-native JSON import (module: Preserve + bundler resolution).
import firecrawlDump from "./firecrawl-plans.json" with { type: "json" };
const items = firecrawlDump.items as ApiPlanV1[];
// Group 1 — Scale (base = scale_tier_1)
export const scaleTier1Base = findById(items, "scale_tier_1");
export const scaleVariants: ApiPlanV1[] = [
findById(items, "scale_tier_2"),
findById(items, "scale_tier_3"),
findById(items, "scale_tier_4"),
findById(items, "scale_tier_1_quarterly"),
findById(items, "scale_tier_2_quarterly"),
findById(items, "scale_tier_3_quarterly"),
findById(items, "scale_tier_4_quarterly"),
findById(items, "scale_monthly"),
];
// Group 2 — Hobby (base = hobby)
export const hobbyBase = findById(items, "hobby");
export const hobbyVariants: ApiPlanV1[] = [
findById(items, "hobby_yearly"),
findById(items, "hobby_monthly_5k"),
findById(items, "hobby_monthly_6_5k"),
findById(items, "hobby_monthly_8k"),
findById(items, "hobby_yearly_5k"),
findById(items, "hobby_yearly_6_5k"),
findById(items, "hobby_yearly_8k"),
];
// Group 3 — Standard (base = standard)
export const standardBase = findById(items, "standard");
export const standardVariants: ApiPlanV1[] = [
findById(items, "standard_yearly"),
findById(items, "standard_monthly_100k"),
findById(items, "standard_monthly_130k"),
findById(items, "standard_monthly_160k"),
findById(items, "standard_yearly_100k"),
findById(items, "standard_yearly_130k"),
findById(items, "standard_yearly_160k"),
];
// Group 4 — Growth (base = growth)
export const growthBase = findById(items, "growth");
export const growthVariants: ApiPlanV1[] = [
findById(items, "growth_yearly"),
findById(items, "growth_monthly_500k"),
findById(items, "growth_monthly_650k"),
findById(items, "growth_monthly_800k"),
findById(items, "growth_yearly_500k"),
findById(items, "growth_yearly_650k"),
];

View File

@@ -0,0 +1,71 @@
import { describe, expect, test } from "bun:test";
import type { ApiPlanV1 } from "@autumn/shared";
import {
applyDiff,
type ApplyDiffOutput,
} from "@autumn/shared/utils/planV1Utils/diff/applyDiff.js";
import {
growthBase,
growthVariants,
hobbyBase,
hobbyVariants,
scaleTier1Base,
scaleVariants,
standardBase,
standardVariants,
} from "./diffPlanV1.firecrawl.fixtures.js";
import { diffPlanV1 } from "@autumn/shared/utils/planV1Utils/diff/diffPlanV1.js";
import { normalizePlan } from "./utils/normalizePlan.js";
// --- test matrix ---
const groups = [
{ name: "scale", base: scaleTier1Base, variants: scaleVariants },
{ name: "hobby", base: hobbyBase, variants: hobbyVariants },
{ name: "standard", base: standardBase, variants: standardVariants },
{ name: "growth", base: growthBase, variants: growthVariants },
];
for (const { name, base, variants } of groups) {
describe(`firecrawl ${name} group — diff/apply round-trip`, () => {
for (const variant of variants) {
test(`${variant.id} reconstructs from ${base.id} + diff`, () => {
const diff = diffPlanV1({ from: base, to: variant });
const reconstructed = applyDiff({ base, diff });
expect(normalizePlan(reconstructed)).toEqual(normalizePlan(variant));
});
}
});
}
describe("filter precision — same-feature-id siblings", () => {
test("mutating the priced CREDITS leaves the price-null CREDITS intact", () => {
const base = growthBase;
const pricedCredits = base.items.find(
(i) => i.feature_id === "CREDITS" && i.price != null,
)!;
const mutated: ApiPlanV1 = {
...base,
items: base.items.map((item) =>
item === pricedCredits
? { ...item, included: item.included + 1 }
: item,
),
};
const diff = diffPlanV1({ from: base, to: mutated });
const reconstructed = applyDiff({ base, diff });
const stillHasPriceNullCredits = reconstructed.items.some(
(i) =>
i.feature_id === "CREDITS" &&
i.price == null &&
i.reset?.interval === "month",
);
expect(stillHasPriceNullCredits).toBe(true);
const mutatedCredits = reconstructed.items.find(
(i) => i.feature_id === "CREDITS" && i.price != null,
);
expect(mutatedCredits?.included).toBe(pricedCredits.included + 1);
});
});

View File

@@ -0,0 +1,695 @@
import type { ApiPlanV1 } from "@autumn/shared";
export const popflyStart = {
"id": "start",
"name": "Run",
"description": null,
"group": null,
"version": 8,
"add_on": false,
"auto_enable": false,
"price": {
"amount": 499,
"interval": "month",
"display": {
"primary_text": "$499",
"secondary_text": "per month"
}
},
"items": [
{
"feature_id": "adventures",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Adventures"
}
},
{
"feature_id": "adventures_visible",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited adventures visible"
}
},
{
"feature_id": "affiliate_programs",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited affiliate programs"
}
},
{
"feature_id": "affiliates_csv_export",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Affiliates CSV Export"
}
},
{
"feature_id": "affiliates_per_program",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited affiliates per program"
}
},
{
"feature_id": "affiliates_reporting",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Affiliates Reporting"
}
},
{
"feature_id": "campaign_progress_management",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Campaign Progress Management"
}
},
{
"feature_id": "campaigns",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Campaigns"
}
},
{
"feature_id": "connections_limit_company_with_company",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited company connections limits"
}
},
{
"feature_id": "company_members",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited company members"
}
},
{
"feature_id": "content",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Content"
}
},
{
"feature_id": "content_storage",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited content storages"
}
},
{
"feature_id": "connections_limit_company_with_creators",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited creator connections limits"
}
},
{
"feature_id": "creator_discovery_advanced",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Creator Discovery Advanced"
}
},
{
"feature_id": "gifting_invitations",
"included": 200,
"unlimited": false,
"reset": {
"interval": "month"
},
"price": null,
"display": {
"primary_text": "200 gifting invitations"
}
},
{
"feature_id": "gifting_products",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited gifting products"
}
},
{
"feature_id": "invite_through_popfly_advanced",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Invite Through Popfly Advanced"
}
},
{
"feature_id": "invoice_fee",
"included": 390,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "390 invoice fees"
}
},
{
"feature_id": "campaigns_private_monthly",
"included": 0,
"unlimited": true,
"reset": {
"interval": "month"
},
"price": null,
"display": {
"primary_text": "Unlimited monthly private campaigns"
}
},
{
"feature_id": "campaigns_public_monthly",
"included": 0,
"unlimited": true,
"reset": {
"interval": "month"
},
"price": null,
"display": {
"primary_text": "Unlimited monthly public campaigns"
}
},
{
"feature_id": "campaigns_unlisted_monthly",
"included": 0,
"unlimited": true,
"reset": {
"interval": "month"
},
"price": null,
"display": {
"primary_text": "Unlimited Monthly Unlisted Campaigns"
}
},
{
"feature_id": "packs",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Packs"
}
},
{
"feature_id": "playbook",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Playbook"
}
},
{
"feature_id": "popfly_platform",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Popfly Platform"
}
},
{
"feature_id": "affiliate_programs_public",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited public affiliate programs"
}
},
{
"feature_id": "social_listening",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Social Listening"
}
},
{
"feature_id": "social_listening_mention_results",
"included": 25,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "25 social listening mention results"
}
},
{
"feature_id": "social_listening_platforms",
"included": 1,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "1 Social Listening Platform"
}
},
{
"feature_id": "social_listening_refresh_frequency_in_hours",
"included": 168,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "168 social listening refresh frequencies (hours)"
}
},
{
"feature_id": "social_listening_terms",
"included": 1,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "1 social listening term"
}
},
{
"feature_id": "social_listening_topics",
"included": 1,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "1 social listening topic"
}
}
],
"created_at": 1777460151677,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
} as ApiPlanV1;
export const popflyStartAnnual = {
"id": "start_annual",
"name": "Run - annual",
"description": null,
"group": null,
"version": 9,
"add_on": false,
"auto_enable": false,
"price": {
"amount": 5988,
"interval": "year",
"display": {
"primary_text": "$5,988",
"secondary_text": "per year"
}
},
"items": [
{
"feature_id": "adventures",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Adventures"
}
},
{
"feature_id": "adventures_visible",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited adventures visible"
}
},
{
"feature_id": "affiliate_programs",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited affiliate programs"
}
},
{
"feature_id": "affiliates_csv_export",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Affiliates CSV Export"
}
},
{
"feature_id": "affiliates_per_program",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited affiliates per program"
}
},
{
"feature_id": "affiliates_reporting",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Affiliates Reporting"
}
},
{
"feature_id": "campaign_progress_management",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Campaign Progress Management"
}
},
{
"feature_id": "campaigns",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Campaigns"
}
},
{
"feature_id": "connections_limit_company_with_company",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited company connections limits"
}
},
{
"feature_id": "company_members",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited company members"
}
},
{
"feature_id": "content",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Content"
}
},
{
"feature_id": "content_storage",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited content storages"
}
},
{
"feature_id": "connections_limit_company_with_creators",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited creator connections limits"
}
},
{
"feature_id": "creator_discovery_advanced",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Creator Discovery Advanced"
}
},
{
"feature_id": "gifting_invitations",
"included": 200,
"unlimited": false,
"reset": {
"interval": "month"
},
"price": null,
"display": {
"primary_text": "200 gifting invitations"
}
},
{
"feature_id": "gifting_products",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited gifting products"
}
},
{
"feature_id": "invite_through_popfly_advanced",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Invite Through Popfly Advanced"
}
},
{
"feature_id": "invoice_fee",
"included": 390,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "390 invoice fees"
}
},
{
"feature_id": "campaigns_private_monthly",
"included": 0,
"unlimited": true,
"reset": {
"interval": "month"
},
"price": null,
"display": {
"primary_text": "Unlimited monthly private campaigns"
}
},
{
"feature_id": "campaigns_public_monthly",
"included": 0,
"unlimited": true,
"reset": {
"interval": "month"
},
"price": null,
"display": {
"primary_text": "Unlimited monthly public campaigns"
}
},
{
"feature_id": "campaigns_unlisted_monthly",
"included": 0,
"unlimited": true,
"reset": {
"interval": "month"
},
"price": null,
"display": {
"primary_text": "Unlimited Monthly Unlisted Campaigns"
}
},
{
"feature_id": "packs",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Packs"
}
},
{
"feature_id": "playbook",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Playbook"
}
},
{
"feature_id": "popfly_platform",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Popfly Platform"
}
},
{
"feature_id": "affiliate_programs_public",
"included": 0,
"unlimited": true,
"reset": null,
"price": null,
"display": {
"primary_text": "Unlimited public affiliate programs"
}
},
{
"feature_id": "social_listening",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Social Listening"
}
},
{
"feature_id": "social_listening_mention_results",
"included": 25,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "25 social listening mention results"
}
},
{
"feature_id": "social_listening_platforms",
"included": 1,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "1 Social Listening Platform"
}
},
{
"feature_id": "social_listening_refresh_frequency_in_hours",
"included": 168,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "168 social listening refresh frequencies (hours)"
}
},
{
"feature_id": "social_listening_terms",
"included": 1,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "1 social listening term"
}
},
{
"feature_id": "social_listening_topics",
"included": 1,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "1 social listening topic"
}
}
],
"created_at": 1777460152188,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
} as ApiPlanV1;

View File

@@ -0,0 +1,102 @@
import { AppEnv, type ApiPlanV1, FreeTrialDuration } from "@autumn/shared";
import { describe, expect, test } from "bun:test";
import { applyDiff } from "@autumn/shared/utils/planV1Utils/diff/applyDiff.js";
import { diffPlanV1 } from "@autumn/shared/utils/planV1Utils/diff/diffPlanV1.js";
const makePlan = (overrides?: Partial<ApiPlanV1>): ApiPlanV1 => ({
id: "test-plan",
name: "Test Plan",
description: null,
group: null,
version: 1,
add_on: false,
auto_enable: false,
price: null,
items: [
{
feature_id: "messages",
included: 0,
unlimited: false,
reset: null,
price: null,
},
],
created_at: 0,
env: AppEnv.Sandbox,
archived: false,
base_variant_id: null,
config: { ignore_past_due: false },
...overrides,
});
describe("diffPlanV1 + applyDiff — free_trial branch", () => {
test("adding a trial produces a diff and apply reconstructs it", () => {
const trial = {
duration_length: 14,
duration_type: FreeTrialDuration.Day,
card_required: false,
};
const from = makePlan({ free_trial: undefined });
const to = makePlan({ free_trial: trial });
const diff = diffPlanV1({ from, to });
expect(diff.free_trial).toEqual(trial);
const result = applyDiff({ base: from, diff });
expect(result.free_trial).toEqual(trial);
});
test("removing a trial produces a null diff and apply drops it", () => {
const trial = {
duration_length: 7,
duration_type: FreeTrialDuration.Day,
card_required: true,
};
const from = makePlan({ free_trial: trial });
const to = makePlan({ free_trial: undefined });
const diff = diffPlanV1({ from, to });
expect(diff.free_trial).toBeNull();
const result = applyDiff({ base: from, diff });
expect(result.free_trial).toBeUndefined();
});
test("changing a trial duration produces a diff and apply updates it", () => {
const fromTrial = {
duration_length: 7,
duration_type: FreeTrialDuration.Day,
card_required: true,
};
const toTrial = {
duration_length: 30,
duration_type: FreeTrialDuration.Day,
card_required: true,
};
const from = makePlan({ free_trial: fromTrial });
const to = makePlan({ free_trial: toTrial });
const diff = diffPlanV1({ from, to });
expect(diff.free_trial).toEqual(toTrial);
const result = applyDiff({ base: from, diff });
expect(result.free_trial).toEqual(toTrial);
});
test("identical trials produce no diff and apply preserves the base", () => {
const trial = {
duration_length: 14,
duration_type: FreeTrialDuration.Day,
card_required: false,
on_end: "bill" as const,
};
const from = makePlan({ free_trial: trial });
const to = makePlan({ free_trial: trial });
const diff = diffPlanV1({ from, to });
expect(diff.free_trial).toBeUndefined();
const result = applyDiff({ base: from, diff });
expect(result.free_trial).toEqual(trial);
});
});

View File

@@ -0,0 +1,15 @@
import { type ApiPlanV1 } from "@autumn/shared";
import { findById } from "./utils/findById.js";
import oneprepDump from "./oneprep-plans.json" with { type: "json" };
const items = oneprepDump.items as ApiPlanV1[];
export const proBase = findById(items, "pro_1m");
export const proVariants: ApiPlanV1[] = [
findById(items, "pro_1w"),
findById(items, "pro_3m"),
findById(items, "pro_6m"),
findById(items, "pro_12m"),
findById(items, "pro_june_2026"),
];

View File

@@ -0,0 +1,20 @@
import { type ApiPlanV1 } from "@autumn/shared";
import { describe, expect, test } from "bun:test";
import { diffPlanV1 } from "@autumn/shared/utils/planV1Utils/diff/diffPlanV1.js";
import { applyDiff } from "@autumn/shared/utils/planV1Utils/diff/applyDiff.js";
import { proBase, proVariants } from "./diffPlanV1.oneprep.fixtures.js";
import { normalizePlan } from "./utils/normalizePlan.js";
const groups = [{ name: "pro", base: proBase, variants: proVariants }];
for (const { name, base, variants } of groups) {
describe(`oneprep ${name} group — diff/apply round-trip`, () => {
for (const variant of variants) {
test(`${variant.id} reconstructs from ${base.id} + diff`, () => {
const diff = diffPlanV1({ from: base, to: variant });
const reconstructed = applyDiff({ base, diff });
expect(normalizePlan(reconstructed)).toEqual(normalizePlan(variant));
});
}
});
}

View File

@@ -0,0 +1,70 @@
import { type ApiPlanV1 } from "@autumn/shared";
import { findById } from "./utils/findById.js";
import revisiondojoDump from "./revisiondojo-plans.json" with { type: "json" };
const items = revisiondojoDump.items as ApiPlanV1[];
// Group 1 — Pro (base = pro_1m)
export const proBase = findById(items, "pro_1m");
export const proVariants: ApiPlanV1[] = [
findById(items, "pro_free_grant"),
findById(items, "pro_1m_new"),
findById(items, "pro_1_month_mobile"),
findById(items, "pro_1w"),
findById(items, "pro_2m"),
findById(items, "pro_3m"),
findById(items, "pro_3m_special"),
findById(items, "pro_4m"),
findById(items, "pro_6m"),
findById(items, "pro_6m_oneoff"),
findById(items, "pro_12m"),
findById(items, "pro_15m"),
findById(items, "pro_18m"),
findById(items, "pro_24m"),
findById(items, "pro_m26"),
findById(items, "pro_2m_m26"),
findById(items, "pro_n26"),
findById(items, "pro_8m_n26"),
findById(items, "pro_m27"),
findById(items, "pro_12m_oneoff"),
findById(items, "pro_14m_m27"),
findById(items, "pro_18m_oneoff"),
findById(items, "pro_n27"),
findById(items, "pro_20m_n27"),
findById(items, "pro_24m_oneoff"),
findById(items, "pro_26m_m28"),
findById(items, "pro_m28"),
];
// Group 2 — Plus (base = plus_1m)
export const plusBase = findById(items, "plus_1m");
export const plusVariants: ApiPlanV1[] = [
findById(items, "plus_free_grant"),
findById(items, "plus_1m_new"),
findById(items, "plus_1w"),
findById(items, "plus_2m"),
findById(items, "plus_3m"),
findById(items, "plus_4m"),
findById(items, "plus_6m"),
findById(items, "plus_12m"),
findById(items, "plus_15m"),
findById(items, "plus_18m"),
findById(items, "plus_24m"),
findById(items, "plus_2m_m26"),
findById(items, "plus_6m_oneoff"),
findById(items, "plus_8m_n26"),
findById(items, "plus_12m_oneoff"),
findById(items, "plus_14m_m27"),
findById(items, "plus_18m_oneoff"),
findById(items, "plus_24m_oneoff"),
findById(items, "plus_26m_m28"),
findById(items, "plus_20m_n27"),
];
// Group 3 — Teacher Pro (base = pro_teacher_1m)
export const teacherProBase = findById(items, "pro_teacher_1m");
export const teacherProVariants: ApiPlanV1[] = [
findById(items, "pro_teacher"),
findById(items, "pro_teacher_24m"),
];

View File

@@ -0,0 +1,32 @@
import { type ApiPlanV1 } from "@autumn/shared";
import { describe, expect, test } from "bun:test";
import { diffPlanV1 } from "@autumn/shared/utils/planV1Utils/diff/diffPlanV1.js";
import { applyDiff } from "@autumn/shared/utils/planV1Utils/diff/applyDiff.js";
import {
proBase,
proVariants,
plusBase,
plusVariants,
teacherProBase,
teacherProVariants,
} from "./diffPlanV1.revisiondojo.fixtures.js";
import { normalizePlan } from "./utils/normalizePlan.js";
// --- test matrix ---
const groups = [
{ name: "pro", base: proBase, variants: proVariants },
{ name: "plus", base: plusBase, variants: plusVariants },
{ name: "teacher_pro", base: teacherProBase, variants: teacherProVariants },
];
for (const { name, base, variants } of groups) {
describe(`revisiondojo ${name} group — diff/apply round-trip`, () => {
for (const variant of variants) {
test(`${variant.id} reconstructs from ${base.id} + diff`, () => {
const diff = diffPlanV1({ from: base, to: variant });
const reconstructed = applyDiff({ base, diff });
expect(normalizePlan(reconstructed)).toEqual(normalizePlan(variant));
});
}
});
}

View File

@@ -0,0 +1,64 @@
import { type ApiPlanV1 } from "@autumn/shared";
import { findById } from "./utils/findById.js";
import runableDump from "./runable-plans.json" with { type: "json" };
const items = runableDump.items as ApiPlanV1[];
// Group 1 — Credit packs (base = runable_pro_25_monthly)
export const creditPackBase = findById(items, "runable_pro_25_monthly");
export const creditPackVariants: ApiPlanV1[] = [
findById(items, "runable_pro_50_monthly"),
findById(items, "runable_pro_75_monthly"),
findById(items, "runable_pro_100_monthly"),
findById(items, "runable_pro_200_monthly"),
findById(items, "runable_pro_300_monthly"),
findById(items, "runable_pro_400_monthly"),
findById(items, "runable_pro_500_monthly"),
findById(items, "runable_pro_750_monthly"),
findById(items, "runable_pro_1000_monthly"),
findById(items, "runable_pro_1500_monthly"),
findById(items, "runable_pro_2000_monthly"),
findById(items, "runable_pro_5000_monthly"),
findById(items, "runable_pro_10000_monthly"),
findById(items, "runable_pro_20000_monthly"),
findById(items, "runable_pro_25_yearly"),
findById(items, "runable_pro_50_yearly"),
findById(items, "runable_pro_75_yearly"),
findById(items, "runable_pro_100_yearly"),
findById(items, "runable_pro_200_yearly"),
findById(items, "runable_pro_300_yearly"),
findById(items, "runable_pro_400_yearly"),
findById(items, "runable_pro_500_yearly"),
findById(items, "runable_pro_750_yearly"),
findById(items, "runable_pro_1000_yearly"),
findById(items, "runable_pro_1500_yearly"),
findById(items, "runable_pro_2000_yearly"),
findById(items, "runable_pro_5000_yearly"),
findById(items, "runable_pro_10000_yearly"),
findById(items, "runable_pro_20000_yearly"),
];
// Group 2 — Plus tier
export const plusBase = findById(items, "runable_plus_monthly");
export const plusVariants: ApiPlanV1[] = [findById(items, "runable_plus_yearly")];
// Group 3 — Pro tier
export const proBase = findById(items, "runable_pro_monthly");
export const proVariants: ApiPlanV1[] = [findById(items, "runable_pro_yearly")];
// Group 4 — Unlimited tier
export const unlimitedBase = findById(items, "runable_unlimited_monthly");
export const unlimitedVariants: ApiPlanV1[] = [findById(items, "runable_unlimited_yearly")];
// Group 5 — Free/starter
export const freeStarterBase = findById(items, "runable_go");
export const freeStarterVariants: ApiPlanV1[] = [
findById(items, "runable_basic"),
findById(items, "runable_starter_monthly"),
findById(items, "runable_starter_yearly"),
];
// Group 6 — Max tier
export const maxBase = findById(items, "runable_max_monthly");
export const maxVariants: ApiPlanV1[] = [findById(items, "runable_max_yearly")];

View File

@@ -0,0 +1,41 @@
import { type ApiPlanV1 } from "@autumn/shared";
import { describe, expect, test } from "bun:test";
import { diffPlanV1 } from "@autumn/shared/utils/planV1Utils/diff/diffPlanV1.js";
import { applyDiff } from "@autumn/shared/utils/planV1Utils/diff/applyDiff.js";
import {
creditPackBase,
creditPackVariants,
plusBase,
plusVariants,
proBase,
proVariants,
unlimitedBase,
unlimitedVariants,
freeStarterBase,
freeStarterVariants,
maxBase,
maxVariants,
} from "./diffPlanV1.runable.fixtures.js";
import { normalizePlan } from "./utils/normalizePlan.js";
// --- test matrix ---
const groups = [
{ name: "credit_pack", base: creditPackBase, variants: creditPackVariants },
{ name: "plus", base: plusBase, variants: plusVariants },
{ name: "pro", base: proBase, variants: proVariants },
{ name: "unlimited", base: unlimitedBase, variants: unlimitedVariants },
{ name: "free_starter", base: freeStarterBase, variants: freeStarterVariants },
{ name: "max", base: maxBase, variants: maxVariants },
];
for (const { name, base, variants } of groups) {
describe(`runable ${name} group — diff/apply round-trip`, () => {
for (const variant of variants) {
test(`${variant.id} reconstructs from ${base.id} + diff`, () => {
const diff = diffPlanV1({ from: base, to: variant });
const reconstructed = applyDiff({ base, diff });
expect(normalizePlan(reconstructed)).toEqual(normalizePlan(variant));
});
}
});
}

View File

@@ -0,0 +1,53 @@
import { type ApiPlanV1, BillingInterval } from "@autumn/shared";
import { describe, expect, test } from "bun:test";
import { diffPlanV1 } from "@autumn/shared/utils/planV1Utils/diff/diffPlanV1.js";
import { popflyStart, popflyStartAnnual } from "./diffPlanV1.fixtures.js";
describe("diffPlanV1 — popfly start vs start_annual", () => {
test("start → start_annual: only price diffs (annual price)", () => {
const diff = diffPlanV1({ from: popflyStart, to: popflyStartAnnual });
expect(diff.price).toEqual({ amount: 5988, interval: BillingInterval.Year });
expect(diff.add_items).toBeUndefined();
expect(diff.remove_items).toBeUndefined();
expect(diff.free_trial).toBeUndefined();
});
test("start → start: empty diff (no fields set)", () => {
const diff = diffPlanV1({ from: popflyStart, to: popflyStart });
expect(diff).toEqual({});
});
test("start_annual → start (reverse): price is the monthly price", () => {
const diff = diffPlanV1({ from: popflyStartAnnual, to: popflyStart });
expect(diff.price).toEqual({ amount: 499, interval: BillingInterval.Month });
expect(diff.add_items).toBeUndefined();
expect(diff.remove_items).toBeUndefined();
expect(diff.free_trial).toBeUndefined();
});
test("modify-in-place: same feature_id with different included → remove + add", () => {
const modified: ApiPlanV1 = {
...popflyStart,
items: popflyStart.items.map((item) =>
item.feature_id === "social_listening_terms"
? { ...item, included: 999 }
: item,
),
};
const diff = diffPlanV1({ from: popflyStart, to: modified });
expect(diff.remove_items).toEqual([
{ feature_id: "social_listening_terms" },
]);
expect(diff.add_items).toHaveLength(1);
expect(diff.add_items?.[0]).toMatchObject({
feature_id: "social_listening_terms",
included: 999,
});
expect(diff.price).toBeUndefined();
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,771 @@
{
"items": [
{
"id": "free",
"name": "Free",
"description": null,
"group": null,
"version": 8,
"add_on": false,
"auto_enable": true,
"price": null,
"items": [
{
"feature_id": "orbs_credit",
"included": 10,
"unlimited": false,
"reset": {
"interval": "week"
},
"price": null,
"display": {
"primary_text": "10 orbs credits"
},
"rollover": {
"max": 20,
"max_percentage": null,
"expiry_duration_type": "forever",
"expiry_duration_length": 1
}
},
{
"feature_id": "orbs_credit",
"included": 10,
"unlimited": false,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "10 orbs credits"
}
},
{
"feature_id": "read_lesson",
"included": 3,
"unlimited": false,
"reset": {
"interval": "week"
},
"price": null,
"display": {
"primary_text": "3 read lessons"
}
},
{
"feature_id": "read_note",
"included": 3,
"unlimited": false,
"reset": {
"interval": "week"
},
"price": null,
"display": {
"primary_text": "3 read notes"
}
},
{
"feature_id": "remix_question_new",
"included": 1,
"unlimited": false,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "1 remix question"
}
}
],
"created_at": 1778484988714,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
},
{
"id": "plus",
"name": "Plus",
"description": null,
"group": null,
"version": 2,
"add_on": false,
"auto_enable": false,
"price": {
"amount": 29,
"interval": "month",
"display": {
"primary_text": "$29",
"secondary_text": "per month"
}
},
"items": [
{
"feature_id": "all_diagnostic_tests",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All diagnostic tests"
}
},
{
"feature_id": "all_predicted_papers",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All predicted papers"
}
},
{
"feature_id": "orbs_credit",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited orbs credits"
}
},
{
"feature_id": "premium_questions",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Questions"
}
}
],
"created_at": 1776913987190,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
},
{
"id": "pro_12m",
"name": "Pro (12 months)",
"description": null,
"group": null,
"version": 4,
"add_on": false,
"auto_enable": false,
"price": {
"amount": 216,
"interval": "month",
"interval_count": 12,
"display": {
"primary_text": "$216",
"secondary_text": "per 12 months"
}
},
"items": [
{
"feature_id": "all_diagnostic_tests",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All diagnostic tests"
}
},
{
"feature_id": "all_predicted_papers",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All predicted papers"
}
},
{
"feature_id": "error_analytics",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Error analytics"
}
},
{
"feature_id": "orbs_credit",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited orbs credits"
}
},
{
"feature_id": "premium_cheatsheets",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Cheatsheets"
}
},
{
"feature_id": "premium_questions",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Questions"
}
},
{
"feature_id": "remix_question_new",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited remix questions"
}
}
],
"created_at": 1778656068706,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
},
{
"id": "pro_1m",
"name": "Pro (1 month)",
"description": null,
"group": null,
"version": 5,
"add_on": false,
"auto_enable": false,
"price": {
"amount": 43.5,
"interval": "month",
"display": {
"primary_text": "$43.5",
"secondary_text": "per month"
}
},
"items": [
{
"feature_id": "all_diagnostic_tests",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All diagnostic tests"
}
},
{
"feature_id": "all_predicted_papers",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All predicted papers"
}
},
{
"feature_id": "error_analytics",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Error analytics"
}
},
{
"feature_id": "orbs_credit",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited orbs credits"
}
},
{
"feature_id": "premium_cheatsheets",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Cheatsheets"
}
},
{
"feature_id": "premium_questions",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Questions"
}
},
{
"feature_id": "remix_question_new",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited remix questions"
}
}
],
"created_at": 1778656038341,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
},
{
"id": "pro_1w",
"name": "Pro (1 week)",
"description": null,
"group": null,
"version": 4,
"add_on": false,
"auto_enable": false,
"price": {
"amount": 28.5,
"interval": "week",
"display": {
"primary_text": "$28.5",
"secondary_text": "per week"
}
},
"items": [
{
"feature_id": "all_diagnostic_tests",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All diagnostic tests"
}
},
{
"feature_id": "all_predicted_papers",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All predicted papers"
}
},
{
"feature_id": "error_analytics",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Error analytics"
}
},
{
"feature_id": "orbs_credit",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited orbs credits"
}
},
{
"feature_id": "premium_cheatsheets",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Cheatsheets"
}
},
{
"feature_id": "premium_questions",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Questions"
}
},
{
"feature_id": "remix_question_new",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited remix questions"
}
}
],
"created_at": 1778656024331,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
},
{
"id": "pro_3m",
"name": "Pro (3 months)",
"description": null,
"group": null,
"version": 4,
"add_on": false,
"auto_enable": false,
"price": {
"amount": 103.5,
"interval": "month",
"interval_count": 3,
"display": {
"primary_text": "$103.5",
"secondary_text": "per 3 months"
}
},
"items": [
{
"feature_id": "all_diagnostic_tests",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All diagnostic tests"
}
},
{
"feature_id": "all_predicted_papers",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All predicted papers"
}
},
{
"feature_id": "error_analytics",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Error analytics"
}
},
{
"feature_id": "orbs_credit",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited orbs credits"
}
},
{
"feature_id": "premium_cheatsheets",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Cheatsheets"
}
},
{
"feature_id": "premium_questions",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Questions"
}
},
{
"feature_id": "remix_question_new",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited remix questions"
}
}
],
"created_at": 1778655992720,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
},
{
"id": "pro_6m",
"name": "Pro (6 months)",
"description": null,
"group": null,
"version": 4,
"add_on": false,
"auto_enable": false,
"price": {
"amount": 144,
"interval": "month",
"interval_count": 6,
"display": {
"primary_text": "$144",
"secondary_text": "per 6 months"
}
},
"items": [
{
"feature_id": "all_diagnostic_tests",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All diagnostic tests"
}
},
{
"feature_id": "all_predicted_papers",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All predicted papers"
}
},
{
"feature_id": "error_analytics",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Error analytics"
}
},
{
"feature_id": "orbs_credit",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited orbs credits"
}
},
{
"feature_id": "premium_cheatsheets",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Cheatsheets"
}
},
{
"feature_id": "premium_questions",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Questions"
}
},
{
"feature_id": "remix_question_new",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited remix questions"
}
}
],
"created_at": 1778655965941,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
},
{
"id": "pro_june_2026",
"name": "Pro (June 2026)",
"description": null,
"group": null,
"version": 3,
"add_on": false,
"auto_enable": false,
"price": {
"amount": 35.99,
"interval": "one_off",
"display": {
"primary_text": "$35.99"
}
},
"items": [
{
"feature_id": "all_diagnostic_tests",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All diagnostic tests"
}
},
{
"feature_id": "all_predicted_papers",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "All predicted papers"
}
},
{
"feature_id": "error_analytics",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Error analytics"
}
},
{
"feature_id": "orbs_credit",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited orbs credits"
}
},
{
"feature_id": "premium_cheatsheets",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Cheatsheets"
}
},
{
"feature_id": "premium_questions",
"included": 0,
"unlimited": false,
"reset": null,
"price": null,
"display": {
"primary_text": "Premium Questions"
}
},
{
"feature_id": "question_remix",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited question remixes"
}
},
{
"feature_id": "remix_question_new",
"included": 0,
"unlimited": true,
"reset": {
"interval": "one_off"
},
"price": null,
"display": {
"primary_text": "Unlimited remix questions"
}
}
],
"created_at": 1779075195611,
"env": "live",
"archived": false,
"base_variant_id": null,
"config": {
"ignore_past_due": false
}
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
export const findById = <T extends { id: string }>(items: T[], id: string): T => {
const item = items.find((p) => p.id === id);
if (!item) throw new Error(`Item not found: ${id}`);
return item;
};

View File

@@ -0,0 +1,72 @@
import type { ApiPlanV1 } from "@autumn/shared";
import type { ApplyDiffOutput } from "@autumn/shared/utils/planV1Utils/diff/applyDiff.js";
export const ITEM_FIELDS = [
"feature_id",
"included",
"unlimited",
"reset",
"price",
"rollover",
] as const;
export type ApiPlanItem = ApiPlanV1["items"][number];
export type NormalizablePlan = {
price: ApiPlanV1["price"];
items: ApiPlanV1["items"];
free_trial?: ApiPlanV1["free_trial"];
};
export const normalizeRollover = (rollover: ApiPlanItem["rollover"]) => {
if (rollover === null || rollover === undefined) return undefined;
const out: Record<string, unknown> = {
expiry_duration_type: rollover.expiry_duration_type,
};
if (rollover.max != null) out.max = rollover.max;
if (rollover.max_percentage != null)
out.max_percentage = rollover.max_percentage;
if (rollover.expiry_duration_length !== undefined)
out.expiry_duration_length = rollover.expiry_duration_length;
return out;
};
export const normalizeItem = (item: ApiPlanItem) => {
const out: Record<string, unknown> = {};
for (const k of ITEM_FIELDS) {
if (k === "rollover") {
const val = item.rollover;
if (val !== undefined && val !== null) out[k] = normalizeRollover(val);
} else {
const val = item[k];
// Diff omits nullish fields in create params; treat null == absent.
if (val !== undefined && val !== null) out[k] = val;
}
}
return out;
};
export const normalizePrice = (price: ApiPlanV1["price"]) => {
if (price === null || price === undefined) return null;
const { display: _d, ...rest } = price;
return rest;
};
export const normalizeFreeTrial = (ft: ApiPlanV1["free_trial"]) => {
if (ft === null || ft === undefined) return null;
const out = { ...ft };
if (out.on_end === null || out.on_end === undefined) delete out.on_end;
return out;
};
export const normalizePlan = (plan: NormalizablePlan | ApplyDiffOutput) => ({
price: normalizePrice(plan.price),
items: [...plan.items]
.sort((a, b) => {
const byFeature = a.feature_id.localeCompare(b.feature_id);
if (byFeature !== 0) return byFeature;
return (a.included ?? 0) - (b.included ?? 0);
})
.map(normalizeItem),
free_trial: normalizeFreeTrial(plan.free_trial),
});