Files
cfw-autumn/server/tests/integration/crud/plans/diffing/utils/normalizePlan.ts
Ayush Rodrigues a8e22d05a9 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>
2026-06-04 12:33:21 +01:00

73 lines
2.2 KiB
TypeScript

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),
});