Files
cfw-autumn/shared/utils/productUtils/priceUtils/match/getPriceStripeReuseLevel.ts

119 lines
3.4 KiB
TypeScript

import {
type Entitlement,
entsAreSame,
isPreviewStripeId,
type Price,
PriceType,
pricesAreSame,
type UsagePriceConfig,
} from "@autumn/shared";
export type PriceStripeReuseLevel = "full" | "stripeProductOnly" | "none";
const stripeResourceFields = [
"stripe_product_id",
"stripe_price_id",
"stripe_empty_price_id",
"stripe_placeholder_price_id",
"stripe_prepaid_price_v2_id",
"stripe_meter_id",
"stripe_event_name",
] as const;
const priceHasPreviewStripeId = ({ price }: { price: Price }) => {
const config = price.config as Partial<
Record<(typeof stripeResourceFields)[number], string | null>
>;
return stripeResourceFields.some((field) =>
isPreviewStripeId({ stripeId: config[field] }),
);
};
const findPairedEntitlement = ({
price,
entitlements,
}: {
price: Price;
entitlements: Entitlement[];
<<<<<<< Updated upstream
}) =>
price.entitlement_id
? entitlements.find(
(entitlement) => entitlement.id === price.entitlement_id,
)
: undefined;
=======
}): Entitlement | undefined =>
priceToEnt({
price,
entitlements: entitlements as EntitlementWithFeature[],
});
>>>>>>> Stashed changes
/**
* Classify how much of the Stripe resource set on `candidatePrice` can be
* carried forward onto `newPrice` when the two represent the same logical
* Autumn price across a plan-update or version transition.
*
* - "full" — config + paired entitlement are identical; reuse all stripe_*_id + stripe_event_name fields.
* - "stripeProductOnly" — same (feature_id, entity_feature_id) usage scope; reuse just stripe_product_id so a new price is created under the existing plan-feature Stripe product.
* - "none" — no reuse, or candidate is preview-only.
*/
export const getPriceStripeReuseLevel = ({
newPrice,
candidatePrice,
newEntitlements,
candidateEntitlements,
}: {
newPrice: Price;
candidatePrice: Price;
newEntitlements: Entitlement[];
candidateEntitlements: Entitlement[];
}): PriceStripeReuseLevel => {
if (priceHasPreviewStripeId({ price: candidatePrice })) return "none";
if (newPrice.config?.type !== candidatePrice.config?.type) return "none";
if (pricesAreSame(candidatePrice, newPrice, false)) {
if (newPrice.config?.type !== PriceType.Usage) return "full";
const newEnt = findPairedEntitlement({
price: newPrice,
entitlements: newEntitlements,
});
const candidateEnt = findPairedEntitlement({
price: candidatePrice,
entitlements: candidateEntitlements,
});
// Both null = same (null) entity scope; both have ents = compare them.
if (!newEnt && !candidateEnt) return "full";
if (!newEnt || !candidateEnt) return "none";
if (entsAreSame(candidateEnt, newEnt)) return "full";
}
if (newPrice.config?.type !== PriceType.Usage) return "none";
const newUsageConfig = newPrice.config as UsagePriceConfig;
const candidateUsageConfig = candidatePrice.config as UsagePriceConfig;
if (newUsageConfig.feature_id !== candidateUsageConfig.feature_id) {
return "none";
}
const newEnt = findPairedEntitlement({
price: newPrice,
entitlements: newEntitlements,
});
const candidateEnt = findPairedEntitlement({
price: candidatePrice,
entitlements: candidateEntitlements,
});
if (!newEnt && !candidateEnt) return "stripeProductOnly";
if (!newEnt || !candidateEnt) return "none";
if (newEnt.entity_feature_id !== candidateEnt.entity_feature_id) {
return "none";
}
return "stripeProductOnly";
};