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:
committed by
Charlie Lamb
parent
aa8f887e11
commit
a8e22d05a9
@@ -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"),
|
||||
];
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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"),
|
||||
];
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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"),
|
||||
];
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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")];
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
2463
server/tests/integration/crud/plans/diffing/firecrawl-plans.json
Normal file
2463
server/tests/integration/crud/plans/diffing/firecrawl-plans.json
Normal file
File diff suppressed because it is too large
Load Diff
771
server/tests/integration/crud/plans/diffing/oneprep-plans.json
Normal file
771
server/tests/integration/crud/plans/diffing/oneprep-plans.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
5642
server/tests/integration/crud/plans/diffing/revisiondojo-plans.json
Normal file
5642
server/tests/integration/crud/plans/diffing/revisiondojo-plans.json
Normal file
File diff suppressed because it is too large
Load Diff
3518
server/tests/integration/crud/plans/diffing/runable-plans.json
Normal file
3518
server/tests/integration/crud/plans/diffing/runable-plans.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
};
|
||||
@@ -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),
|
||||
});
|
||||
Reference in New Issue
Block a user