versioning api plan
This commit is contained in:
@@ -10,6 +10,7 @@ BUN_PARALLEL_V2 \
|
||||
'integration/billing/migrations' \
|
||||
'integration/billing/cron' \
|
||||
'integration/crud/customers' \
|
||||
'integration/crud/plans' \
|
||||
'integration/billing/attach' \
|
||||
|
||||
|
||||
|
||||
5
server/src/external/autumn/autumnCli.ts
vendored
5
server/src/external/autumn/autumnCli.ts
vendored
@@ -580,6 +580,11 @@ export class AutumnInt {
|
||||
return data;
|
||||
},
|
||||
|
||||
list: async <T = any[]>(): Promise<{ list: T }> => {
|
||||
const data = await this.get(`/products`);
|
||||
return data as { list: T };
|
||||
},
|
||||
|
||||
create: async (product: any) => {
|
||||
const data = await this.post(`/products`, product);
|
||||
return data;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiCustomer,
|
||||
type ApiEntityV1,
|
||||
type ApiPlan,
|
||||
type ApiCustomerV5,
|
||||
type ApiEntityV2,
|
||||
type ApiPlanV1,
|
||||
ApiVersion,
|
||||
ApiVersionClass,
|
||||
type AppEnv,
|
||||
@@ -150,7 +150,7 @@ export const handleProductsUpdated = async ({
|
||||
});
|
||||
|
||||
const versionedCustomer = applyResponseVersionChanges<
|
||||
ApiCustomer,
|
||||
ApiCustomerV5,
|
||||
CustomerLegacyData
|
||||
>({
|
||||
input: apiCustomer,
|
||||
@@ -165,7 +165,7 @@ export const handleProductsUpdated = async ({
|
||||
features,
|
||||
});
|
||||
|
||||
const versionedPlan = applyResponseVersionChanges<ApiPlan, PlanLegacyData>({
|
||||
const versionedPlan = applyResponseVersionChanges<ApiPlanV1, PlanLegacyData>({
|
||||
input: apiPlan,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Product,
|
||||
@@ -183,7 +183,7 @@ export const handleProductsUpdated = async ({
|
||||
fullCus,
|
||||
});
|
||||
|
||||
entity = applyResponseVersionChanges<ApiEntityV1, EntityLegacyData>({
|
||||
entity = applyResponseVersionChanges<ApiEntityV2, EntityLegacyData>({
|
||||
input: apiEntity,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Entity,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
type ApiCustomer,
|
||||
type ApiEntityV1,
|
||||
type ApiCustomerV5,
|
||||
type ApiEntityV2,
|
||||
type CheckParams,
|
||||
type CustomerLegacyData,
|
||||
type Feature,
|
||||
@@ -43,7 +43,7 @@ const getFeatureToUse = ({
|
||||
}: {
|
||||
creditSystems: Feature[];
|
||||
feature: Feature;
|
||||
apiEntity: ApiCustomer | ApiEntityV1;
|
||||
apiEntity: ApiCustomerV5 | ApiEntityV2;
|
||||
requiredBalance: number;
|
||||
}) => {
|
||||
// 1. If there's a credit system & cusEnts for that credit system -> return credit system
|
||||
@@ -104,7 +104,7 @@ export const getCheckData = async ({
|
||||
throw new FeatureNotFoundError({ featureId: feature_id });
|
||||
}
|
||||
|
||||
let apiEntity: ApiCustomer | ApiEntityV1 | undefined;
|
||||
let apiEntity: ApiCustomerV5 | ApiEntityV2 | undefined;
|
||||
let legacyData: CustomerLegacyData | undefined;
|
||||
const start = performance.now();
|
||||
const fullCustomer = await getOrCreateCachedFullCustomer({
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
import {
|
||||
type CheckoutChange,
|
||||
CusExpand,
|
||||
cusProductToProduct,
|
||||
type FullCusProduct,
|
||||
type FullProduct,
|
||||
isPrepaidPrice,
|
||||
orgToCurrency,
|
||||
} from "@autumn/shared";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { cusProductToBalances } from "@/internal/customers/cusUtils/apiCusUtils/getApiBalance/cusProductToBalances.js";
|
||||
import { getPlanResponse } from "@/internal/products/productUtils/productResponseUtils/getPlanResponse.js";
|
||||
import type { AttachParams } from "../../customers/cusProducts/AttachParams.js";
|
||||
import { type FullCusProduct, isPrepaidPrice } from "@autumn/shared";
|
||||
|
||||
/**
|
||||
* Convert cusProduct.options to feature_quantities with actual quantities
|
||||
@@ -48,123 +36,3 @@ function cusProductToFeatureQuantities({
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build incoming change from the new product being attached
|
||||
*/
|
||||
async function buildIncomingChange({
|
||||
ctx,
|
||||
attachParams,
|
||||
newProduct,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
attachParams: AttachParams;
|
||||
newProduct: FullProduct;
|
||||
}): Promise<CheckoutChange> {
|
||||
const currency = orgToCurrency({ org: ctx.org });
|
||||
|
||||
const plan = await getPlanResponse({
|
||||
product: newProduct,
|
||||
features: ctx.features,
|
||||
fullCus: attachParams.customer,
|
||||
currency,
|
||||
expand: [CusExpand.PlanFeaturesFeature],
|
||||
});
|
||||
|
||||
// Build feature quantities from attach options
|
||||
const featureQuantities = attachParams.optionsList.map((option) => ({
|
||||
feature_id: option.feature_id,
|
||||
quantity: option.quantity,
|
||||
}));
|
||||
|
||||
return {
|
||||
plan,
|
||||
feature_quantities: featureQuantities,
|
||||
balances: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outgoing change from the current product being replaced
|
||||
*/
|
||||
async function buildOutgoingChange({
|
||||
ctx,
|
||||
attachParams,
|
||||
curCusProduct,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
attachParams: AttachParams;
|
||||
curCusProduct: FullCusProduct;
|
||||
}): Promise<CheckoutChange> {
|
||||
const currency = orgToCurrency({ org: ctx.org });
|
||||
const fullProduct = cusProductToProduct({ cusProduct: curCusProduct });
|
||||
|
||||
const plan = await getPlanResponse({
|
||||
product: fullProduct,
|
||||
features: ctx.features,
|
||||
fullCus: attachParams.customer,
|
||||
currency,
|
||||
expand: [CusExpand.PlanFeaturesFeature],
|
||||
});
|
||||
|
||||
const balances = cusProductToBalances({
|
||||
ctx,
|
||||
cusProduct: curCusProduct,
|
||||
fullCustomer: attachParams.customer,
|
||||
});
|
||||
|
||||
const featureQuantities = cusProductToFeatureQuantities({
|
||||
cusProduct: curCusProduct,
|
||||
});
|
||||
|
||||
return {
|
||||
plan,
|
||||
feature_quantities: featureQuantities,
|
||||
balances,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert attach params to incoming and outgoing CheckoutChange arrays.
|
||||
* Incoming = product being attached, Outgoing = product being replaced (if any).
|
||||
*/
|
||||
export const attachParamsToChanges = async ({
|
||||
ctx,
|
||||
attachParams,
|
||||
curCusProduct,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
attachParams: AttachParams;
|
||||
curCusProduct?: FullCusProduct;
|
||||
}): Promise<{ incoming: CheckoutChange[]; outgoing: CheckoutChange[] }> => {
|
||||
const incoming: CheckoutChange[] = [];
|
||||
const outgoing: CheckoutChange[] = [];
|
||||
|
||||
// Build new product from attach params
|
||||
const newProduct: FullProduct = {
|
||||
...attachParams.products[0],
|
||||
prices: attachParams.prices,
|
||||
entitlements: attachParams.entitlements,
|
||||
free_trial: attachParams.freeTrial,
|
||||
};
|
||||
|
||||
// Always add incoming (the new product being attached)
|
||||
const incomingChange = await buildIncomingChange({
|
||||
ctx,
|
||||
attachParams,
|
||||
newProduct,
|
||||
});
|
||||
incoming.push(incomingChange);
|
||||
|
||||
// Add outgoing if there's a current product being replaced
|
||||
if (curCusProduct) {
|
||||
const outgoingChange = await buildOutgoingChange({
|
||||
ctx,
|
||||
attachParams,
|
||||
curCusProduct,
|
||||
});
|
||||
outgoing.push(outgoingChange);
|
||||
}
|
||||
|
||||
return { incoming, outgoing };
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ import { getNewProductPreview } from "@/internal/customers/attach/handleAttachPr
|
||||
import { getUpgradeProductPreview } from "@/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.js";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import type { AutumnContext } from "../../../honoUtils/HonoEnv";
|
||||
import { attachParamsToChanges } from "./attachParamsToChanges.js";
|
||||
|
||||
export const attachParamsToPreview = async ({
|
||||
ctx,
|
||||
@@ -102,13 +101,6 @@ export const attachParamsToPreview = async ({
|
||||
const { curScheduledProduct } = attachParamToCusProducts({ attachParams });
|
||||
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
|
||||
|
||||
// Compute incoming/outgoing changes for the UI
|
||||
const { incoming, outgoing } = await attachParamsToChanges({
|
||||
ctx,
|
||||
attachParams,
|
||||
curCusProduct,
|
||||
});
|
||||
|
||||
return {
|
||||
branch,
|
||||
func,
|
||||
@@ -119,7 +111,5 @@ export const attachParamsToPreview = async ({
|
||||
})
|
||||
: null,
|
||||
scheduled_product: curScheduledProduct,
|
||||
incoming,
|
||||
outgoing,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {
|
||||
addToExpand,
|
||||
type BillingContext,
|
||||
type BillingPeriod,
|
||||
type BillingPlan,
|
||||
type CheckoutChange,
|
||||
CusExpand,
|
||||
CusProductStatus,
|
||||
type FullCusProduct,
|
||||
isPrepaidPrice,
|
||||
@@ -12,7 +10,7 @@ import {
|
||||
} from "@autumn/shared";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { cusProductToBalances } from "@/internal/customers/cusUtils/apiCusUtils/getApiBalance/cusProductToBalances.js";
|
||||
import { getApiSubscriptionForCheckout } from "./getApiSubscriptionForCheckout.js";
|
||||
import { getApiSubscription } from "@/internal/customers/cusUtils/apiCusUtils/getApiSubscription/getApiSubscription.js";
|
||||
|
||||
/**
|
||||
* Convert cusProduct.options to feature_quantities with actual quantities
|
||||
@@ -87,19 +85,16 @@ export const billingPlanToChanges = async ({
|
||||
const outgoing: CheckoutChange[] = [];
|
||||
const { autumn } = billingPlan;
|
||||
const { fullCustomer } = billingContext;
|
||||
const ctxWithExpand = addToExpand({
|
||||
ctx,
|
||||
add: [CusExpand.SubscriptionsPlan],
|
||||
});
|
||||
|
||||
const lineItems = autumn.lineItems ?? [];
|
||||
|
||||
// 1. Products being added (incoming)
|
||||
for (const cusProduct of autumn.insertCustomerProducts) {
|
||||
const subscription = await getApiSubscriptionForCheckout({
|
||||
ctx: ctxWithExpand,
|
||||
const { data: subscription } = await getApiSubscription({
|
||||
ctx,
|
||||
cusProduct,
|
||||
billingContext,
|
||||
fullCus: fullCustomer,
|
||||
expandParams: { plan: true },
|
||||
});
|
||||
|
||||
const balances = cusProductToBalances({
|
||||
@@ -132,10 +127,11 @@ export const billingPlanToChanges = async ({
|
||||
updates.ended_at ||
|
||||
updates.status === CusProductStatus.Expired
|
||||
) {
|
||||
const subscription = await getApiSubscriptionForCheckout({
|
||||
const { data: subscription } = await getApiSubscription({
|
||||
ctx,
|
||||
cusProduct: customerProduct,
|
||||
billingContext,
|
||||
fullCus: fullCustomer,
|
||||
expandParams: { plan: true },
|
||||
});
|
||||
|
||||
const balances = cusProductToBalances({
|
||||
@@ -165,10 +161,11 @@ export const billingPlanToChanges = async ({
|
||||
if (autumn.deleteCustomerProduct) {
|
||||
const cusProduct = autumn.deleteCustomerProduct;
|
||||
|
||||
const subscription = await getApiSubscriptionForCheckout({
|
||||
const { data: subscription } = await getApiSubscription({
|
||||
ctx,
|
||||
cusProduct,
|
||||
billingContext,
|
||||
fullCus: fullCustomer,
|
||||
expandParams: { plan: true },
|
||||
});
|
||||
|
||||
const balances = cusProductToBalances({
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import {
|
||||
type BillingContext,
|
||||
type CheckoutSubscription,
|
||||
CusExpand,
|
||||
CusProductStatus,
|
||||
cusProductToPlanStatus,
|
||||
cusProductToProduct,
|
||||
type FullCusProduct,
|
||||
isCustomerProductTrialing,
|
||||
orgToCurrency,
|
||||
secondsToMs,
|
||||
} from "@autumn/shared";
|
||||
import {
|
||||
getEarliestPeriodStart,
|
||||
getLatestPeriodEnd,
|
||||
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { getPlanResponse } from "@/internal/products/productUtils/productResponseUtils/getPlanResponse.js";
|
||||
|
||||
/**
|
||||
* Build an ApiSubscription with plan always included (for checkout display).
|
||||
* Unlike getApiSubscription which uses ctx.expand, this always includes the plan.
|
||||
*/
|
||||
export const getApiSubscriptionForCheckout = async ({
|
||||
ctx,
|
||||
cusProduct,
|
||||
billingContext,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
cusProduct: FullCusProduct;
|
||||
billingContext: BillingContext;
|
||||
}): Promise<CheckoutSubscription> => {
|
||||
const fullProduct = cusProductToProduct({ cusProduct });
|
||||
const { fullCustomer, stripeSubscription } = billingContext;
|
||||
const currency = orgToCurrency({ org: ctx.org });
|
||||
|
||||
// Always get plan for checkout (with features expanded for display)
|
||||
const plan = await getPlanResponse({
|
||||
product: fullProduct,
|
||||
features: ctx.features,
|
||||
fullCus: fullCustomer,
|
||||
currency,
|
||||
expand: [CusExpand.PlanFeaturesFeature],
|
||||
});
|
||||
|
||||
const status = cusProductToPlanStatus({ status: cusProduct.status });
|
||||
|
||||
// Get subscription period from Stripe subscription if available
|
||||
let periodStart: number | null = null;
|
||||
let periodEnd: number | null = null;
|
||||
|
||||
if (stripeSubscription) {
|
||||
periodStart =
|
||||
secondsToMs(getEarliestPeriodStart({ sub: stripeSubscription })) ?? null;
|
||||
periodEnd =
|
||||
secondsToMs(getLatestPeriodEnd({ sub: stripeSubscription })) ?? null;
|
||||
} else if (
|
||||
cusProduct.trial_ends_at &&
|
||||
cusProduct.trial_ends_at > Date.now()
|
||||
) {
|
||||
periodStart = cusProduct.starts_at;
|
||||
periodEnd = cusProduct.trial_ends_at;
|
||||
}
|
||||
|
||||
return {
|
||||
plan,
|
||||
plan_id: fullProduct.id,
|
||||
add_on: fullProduct.is_add_on,
|
||||
default: fullProduct.is_default,
|
||||
|
||||
status,
|
||||
past_due: cusProduct.status === CusProductStatus.PastDue,
|
||||
canceled_at: cusProduct.canceled_at || null,
|
||||
expires_at: cusProduct.ended_at || null,
|
||||
|
||||
trial_ends_at: isCustomerProductTrialing(cusProduct)
|
||||
? (cusProduct.trial_ends_at ?? null)
|
||||
: null,
|
||||
started_at: cusProduct.starts_at,
|
||||
quantity: cusProduct.quantity,
|
||||
current_period_start: periodStart,
|
||||
current_period_end: periodEnd,
|
||||
};
|
||||
};
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiCustomer,
|
||||
type ApiEntityV1,
|
||||
type ApiPlan,
|
||||
type ApiCustomerV5,
|
||||
type ApiEntityV2,
|
||||
type ApiPlanV1,
|
||||
ApiVersion,
|
||||
ApiVersionClass,
|
||||
addToExpand,
|
||||
@@ -94,7 +94,7 @@ export const sendProductsUpdated = async ({
|
||||
});
|
||||
|
||||
const versionedCustomer = applyResponseVersionChanges<
|
||||
ApiCustomer,
|
||||
ApiCustomerV5,
|
||||
CustomerLegacyData
|
||||
>({
|
||||
input: apiCustomer,
|
||||
@@ -109,7 +109,7 @@ export const sendProductsUpdated = async ({
|
||||
features,
|
||||
});
|
||||
|
||||
const versionedPlan = applyResponseVersionChanges<ApiPlan, PlanLegacyData>({
|
||||
const versionedPlan = applyResponseVersionChanges<ApiPlanV1, PlanLegacyData>({
|
||||
input: apiPlan,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Product,
|
||||
@@ -127,7 +127,7 @@ export const sendProductsUpdated = async ({
|
||||
fullCus: fullCustomer,
|
||||
});
|
||||
|
||||
entity = applyResponseVersionChanges<ApiEntityV1, EntityLegacyData>({
|
||||
entity = applyResponseVersionChanges<ApiEntityV2, EntityLegacyData>({
|
||||
input: apiEntity,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Entity,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiCustomer,
|
||||
type ApiCustomerV5,
|
||||
applyResponseVersionChanges,
|
||||
CusExpand,
|
||||
type CustomerLegacyData,
|
||||
@@ -21,7 +21,7 @@ export const getApiCustomer = async ({
|
||||
ctx: RequestContext;
|
||||
fullCustomer: FullCustomer;
|
||||
withAutumnId?: boolean;
|
||||
}): Promise<ApiCustomer> => {
|
||||
}): Promise<ApiCustomerV5> => {
|
||||
// Get base ApiCustomer (subscriptions, balances, invoices)
|
||||
const { apiCustomer: baseCustomer, legacyData: customerLegacyData } =
|
||||
await getApiCustomerBase({
|
||||
@@ -31,7 +31,7 @@ export const getApiCustomer = async ({
|
||||
});
|
||||
|
||||
// Clean base customer (remove entities from base, handle expand)
|
||||
const cleanedBaseCustomer: ApiCustomer = {
|
||||
const cleanedBaseCustomer: ApiCustomerV5 = {
|
||||
...baseCustomer,
|
||||
entities: undefined,
|
||||
autumn_id: withAutumnId ? baseCustomer.autumn_id : undefined,
|
||||
@@ -47,13 +47,13 @@ export const getApiCustomer = async ({
|
||||
fullCus: fullCustomer,
|
||||
});
|
||||
|
||||
const apiCustomer: ApiCustomer = {
|
||||
const apiCustomer: ApiCustomerV5 = {
|
||||
...cleanedBaseCustomer,
|
||||
...apiCustomerExpand,
|
||||
};
|
||||
|
||||
// Apply version transformations based on API version
|
||||
return applyResponseVersionChanges<ApiCustomer, CustomerLegacyData>({
|
||||
return applyResponseVersionChanges<ApiCustomerV5, CustomerLegacyData>({
|
||||
input: apiCustomer,
|
||||
legacyData: customerLegacyData,
|
||||
targetVersion: ctx.apiVersion,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
type ApiCustomer,
|
||||
ApiCustomerSchema,
|
||||
type ApiCustomerV5,
|
||||
ApiCustomerV5Schema,
|
||||
CusExpand,
|
||||
type CustomerLegacyData,
|
||||
type FullCustomer,
|
||||
@@ -24,7 +24,7 @@ export const getApiCustomerBase = async ({
|
||||
ctx: RequestContext;
|
||||
fullCus: FullCustomer;
|
||||
withAutumnId?: boolean;
|
||||
}): Promise<{ apiCustomer: ApiCustomer; legacyData: CustomerLegacyData }> => {
|
||||
}): Promise<{ apiCustomer: ApiCustomerV5; legacyData: CustomerLegacyData }> => {
|
||||
const { data: apiBalances, legacyData: cusFeatureLegacyData } =
|
||||
await getApiBalances({
|
||||
ctx,
|
||||
@@ -37,7 +37,7 @@ export const getApiCustomerBase = async ({
|
||||
fullCus,
|
||||
});
|
||||
|
||||
const apiCustomer = ApiCustomerSchema.extend({
|
||||
const apiCustomer = ApiCustomerV5Schema.extend({
|
||||
autumn_id: z.string().optional(),
|
||||
}).parse({
|
||||
autumn_id: withAutumnId ? fullCus.internal_id : undefined,
|
||||
@@ -50,14 +50,9 @@ export const getApiCustomerBase = async ({
|
||||
|
||||
stripe_id: fullCus.processor?.id || null,
|
||||
env: fullCus.env,
|
||||
metadata: fullCus.metadata,
|
||||
|
||||
// subscriptions: apiSubscriptions,
|
||||
subscriptions: apiSubscriptions.filter((s) => s.status === "active"),
|
||||
scheduled_subscriptions: apiSubscriptions.filter(
|
||||
(s) => s.status === "scheduled",
|
||||
),
|
||||
metadata: fullCus.metadata ?? {},
|
||||
|
||||
subscriptions: apiSubscriptions,
|
||||
balances: apiBalances,
|
||||
send_email_receipts: fullCus.send_email_receipts ?? false,
|
||||
|
||||
@@ -67,7 +62,7 @@ export const getApiCustomerBase = async ({
|
||||
invoices: fullCus.invoices,
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
} satisfies ApiCustomerV5);
|
||||
|
||||
return {
|
||||
apiCustomer,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
ApiSubscriptionSchema,
|
||||
type ApiPlanV1,
|
||||
type ApiSubscriptionV1,
|
||||
ApiSubscriptionV1Schema,
|
||||
CusExpand,
|
||||
type CusProductLegacyData,
|
||||
CusProductStatus,
|
||||
@@ -14,15 +16,29 @@ import {
|
||||
import type { RequestContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { getPlanResponse } from "@/internal/products/productUtils/productResponseUtils/getPlanResponse.js";
|
||||
|
||||
export const getApiSubscription = async ({
|
||||
type SubscriptionExpandParams = { plan?: boolean };
|
||||
|
||||
type ApiSubscriptionResult<T extends SubscriptionExpandParams> = {
|
||||
data: T["plan"] extends true
|
||||
? ApiSubscriptionV1 & { plan: ApiPlanV1 }
|
||||
: ApiSubscriptionV1;
|
||||
legacyData: CusProductLegacyData;
|
||||
};
|
||||
|
||||
export const getApiSubscription = async <
|
||||
// biome-ignore lint/complexity/noBannedTypes: required for type inference
|
||||
T extends SubscriptionExpandParams = {},
|
||||
>({
|
||||
ctx,
|
||||
fullCus,
|
||||
cusProduct,
|
||||
expandParams,
|
||||
}: {
|
||||
ctx: RequestContext;
|
||||
fullCus: FullCustomer;
|
||||
cusProduct: FullCusProduct;
|
||||
}) => {
|
||||
expandParams?: T;
|
||||
}): Promise<ApiSubscriptionResult<T>> => {
|
||||
const trialing =
|
||||
cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
|
||||
|
||||
@@ -62,9 +78,10 @@ export const getApiSubscription = async ({
|
||||
const status = cusProductToPlanStatus({ status: cusProduct.status });
|
||||
|
||||
// Check if we should expand the plan object
|
||||
|
||||
// Use expandParams.plan if provided, otherwise fall back to ctx.expand
|
||||
const shouldExpandPlan =
|
||||
status === "scheduled"
|
||||
expandParams?.plan ??
|
||||
(status === "scheduled"
|
||||
? expandIncludes({
|
||||
expand: ctx.expand,
|
||||
includes: [CusExpand.ScheduledSubscriptionsPlan],
|
||||
@@ -72,7 +89,7 @@ export const getApiSubscription = async ({
|
||||
: expandIncludes({
|
||||
expand: ctx.expand,
|
||||
includes: [CusExpand.SubscriptionsPlan],
|
||||
});
|
||||
}));
|
||||
|
||||
const apiPlan = shouldExpandPlan
|
||||
? await getPlanResponse({
|
||||
@@ -81,12 +98,12 @@ export const getApiSubscription = async ({
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const apiSubscription = ApiSubscriptionSchema.parse({
|
||||
const apiSubscription = ApiSubscriptionV1Schema.parse({
|
||||
plan: apiPlan,
|
||||
|
||||
plan_id: fullProduct.id,
|
||||
add_on: fullProduct.is_add_on,
|
||||
default: fullProduct.is_default,
|
||||
auto_enable: fullProduct.is_default,
|
||||
|
||||
status,
|
||||
past_due: cusProduct.status === CusProductStatus.PastDue,
|
||||
@@ -94,25 +111,19 @@ export const getApiSubscription = async ({
|
||||
expires_at: cusProduct.ended_at || null,
|
||||
|
||||
trial_ends_at: isCustomerProductTrialing(cusProduct)
|
||||
? cusProduct.trial_ends_at
|
||||
? (cusProduct.trial_ends_at ?? null)
|
||||
: null,
|
||||
started_at: cusProduct.starts_at,
|
||||
quantity: cusProduct.quantity,
|
||||
current_period_start: stripeSubData?.current_period_start || null,
|
||||
current_period_end: stripeSubData?.current_period_end || null,
|
||||
feature_quantities: cusProduct.options.map((option) => ({
|
||||
feature_id: option.feature_id,
|
||||
quantity: option.quantity,
|
||||
upcoming_quantity: option.upcoming_quantity,
|
||||
})),
|
||||
});
|
||||
} satisfies ApiSubscriptionV1);
|
||||
|
||||
return {
|
||||
data: apiSubscription,
|
||||
legacyData: {
|
||||
subscription_id: subId || undefined,
|
||||
options: cusProduct.options,
|
||||
// features: ctx.features,
|
||||
} satisfies CusProductLegacyData,
|
||||
};
|
||||
} as ApiSubscriptionResult<T>;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
ACTIVE_STATUSES,
|
||||
type ApiSubscription,
|
||||
type ApiSubscriptionV1,
|
||||
type CusProductLegacyData,
|
||||
type CusProductStatus,
|
||||
type FullCustomer,
|
||||
@@ -11,16 +11,16 @@ import { getApiSubscription } from "./getApiSubscription.js";
|
||||
const mergeSubscriptionsResponses = ({
|
||||
subscriptions,
|
||||
}: {
|
||||
subscriptions: ApiSubscription[];
|
||||
subscriptions: ApiSubscriptionV1[];
|
||||
}) => {
|
||||
const getPlanKey = (cp: ApiSubscription) => {
|
||||
const getPlanKey = (cp: ApiSubscriptionV1) => {
|
||||
const status = ACTIVE_STATUSES.includes(cp.status as CusProductStatus)
|
||||
? "active"
|
||||
: cp.status;
|
||||
return `${cp.plan_id}:${status}`;
|
||||
};
|
||||
|
||||
const record: Record<string, any> = {};
|
||||
const record: Record<string, ApiSubscriptionV1> = {};
|
||||
|
||||
for (const curr of subscriptions) {
|
||||
const key = getPlanKey(curr);
|
||||
@@ -51,7 +51,8 @@ export const getApiSubscriptions = async ({
|
||||
fullCus: FullCustomer;
|
||||
}) => {
|
||||
// Process full subscriptions
|
||||
const apiSubs: ApiSubscription[] = [];
|
||||
const apiSubs: ApiSubscriptionV1[] = [];
|
||||
// const apiPurchases: ApiPurchaseV0[] = [];
|
||||
|
||||
const cusProducts = fullCus.customer_products;
|
||||
|
||||
@@ -63,6 +64,8 @@ export const getApiSubscriptions = async ({
|
||||
fullCus,
|
||||
});
|
||||
|
||||
apiSubs.push(processed.data);
|
||||
|
||||
apiSubs.push(processed.data);
|
||||
legacyData[processed.data.plan_id] = processed.legacyData;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiEntityV1,
|
||||
type ApiEntityV2,
|
||||
applyResponseVersionChanges,
|
||||
type EntityLegacyData,
|
||||
EntityNotFoundError,
|
||||
@@ -26,7 +26,7 @@ export const getApiEntity = async ({
|
||||
entityId: string;
|
||||
fullCus?: FullCustomer;
|
||||
withAutumnId?: boolean;
|
||||
}): Promise<ApiEntityV1> => {
|
||||
}): Promise<ApiEntityV2> => {
|
||||
const fullCustomer =
|
||||
fullCus ??
|
||||
(await getOrSetCachedFullCustomer({
|
||||
@@ -69,7 +69,7 @@ export const getApiEntity = async ({
|
||||
...apiEntityExpand,
|
||||
};
|
||||
|
||||
return applyResponseVersionChanges<ApiEntityV1, EntityLegacyData>({
|
||||
return applyResponseVersionChanges<ApiEntityV2, EntityLegacyData>({
|
||||
input: apiEntity,
|
||||
legacyData: entityLegacyData,
|
||||
targetVersion: ctx.apiVersion,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
type ApiEntityV1,
|
||||
ApiEntityV1Schema,
|
||||
type ApiEntityV2,
|
||||
ApiEntityV2Schema,
|
||||
type Entity,
|
||||
type EntityLegacyData,
|
||||
type FullCustomer,
|
||||
@@ -25,7 +25,7 @@ export const getApiEntityBase = async ({
|
||||
entity: Entity;
|
||||
fullCus: FullCustomer;
|
||||
withAutumnId?: boolean;
|
||||
}): Promise<{ apiEntity: ApiEntityV1; legacyData: EntityLegacyData }> => {
|
||||
}): Promise<{ apiEntity: ApiEntityV2; legacyData: EntityLegacyData }> => {
|
||||
const { org } = ctx;
|
||||
|
||||
// Filter customer products for this entity
|
||||
@@ -55,7 +55,7 @@ export const getApiEntityBase = async ({
|
||||
fullCus: filteredFullCus,
|
||||
});
|
||||
|
||||
const apiEntity = ApiEntityV1Schema.extend({
|
||||
const apiEntity = ApiEntityV2Schema.extend({
|
||||
autumn_id: z.string().optional(),
|
||||
}).parse({
|
||||
autumn_id: withAutumnId ? entity.internal_id : undefined,
|
||||
@@ -66,12 +66,9 @@ export const getApiEntityBase = async ({
|
||||
created_at: entity.created_at,
|
||||
env: fullCus.env,
|
||||
|
||||
subscriptions: apiSubscriptions.filter((s) => s.status === "active"),
|
||||
scheduled_subscriptions: apiSubscriptions.filter(
|
||||
(s) => s.status === "scheduled",
|
||||
),
|
||||
subscriptions: apiSubscriptions,
|
||||
balances: apiBalances,
|
||||
});
|
||||
} satisfies ApiEntityV2);
|
||||
|
||||
return {
|
||||
apiEntity,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiPlan,
|
||||
type ApiPlanV1,
|
||||
ApiVersion,
|
||||
apiPlan,
|
||||
applyResponseVersionChanges,
|
||||
@@ -147,7 +147,7 @@ export const handleCreatePlan = createRoute({
|
||||
});
|
||||
|
||||
// Apply version transformations for client
|
||||
const versionedResponse = applyResponseVersionChanges<ApiPlan>({
|
||||
const versionedResponse = applyResponseVersionChanges<ApiPlanV1>({
|
||||
input: planResponse,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Product,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiPlan,
|
||||
type ApiPlanV1,
|
||||
applyResponseVersionChanges,
|
||||
ErrCode,
|
||||
ProductNotFoundError,
|
||||
@@ -57,7 +57,7 @@ export const handleGetPlan = createRoute({
|
||||
features,
|
||||
});
|
||||
|
||||
const versionedResponse = applyResponseVersionChanges<ApiPlan>({
|
||||
const versionedResponse = applyResponseVersionChanges<ApiPlanV1>({
|
||||
input: planResponse,
|
||||
targetVersion: apiVersion,
|
||||
resource: AffectedResource.Product,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiPlan,
|
||||
type ApiPlanV1,
|
||||
applyResponseVersionChanges,
|
||||
ListPlansQuerySchema,
|
||||
} from "@autumn/shared";
|
||||
@@ -56,19 +56,12 @@ export const handleListPlans = createRoute({
|
||||
db,
|
||||
currency: org.default_currency || undefined,
|
||||
}),
|
||||
// getProductResponse({
|
||||
// product: p,
|
||||
// features,
|
||||
// currency: org.default_currency || undefined,
|
||||
// db,
|
||||
// fullCus: customer ? customer : undefined,
|
||||
// }),
|
||||
);
|
||||
}
|
||||
|
||||
const plansList = await Promise.all(batchResponse);
|
||||
const res = plansList.map((p) => {
|
||||
return applyResponseVersionChanges<ApiPlan>({
|
||||
return applyResponseVersionChanges<ApiPlanV1>({
|
||||
input: p,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Product,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiPlan,
|
||||
type ApiPlanV1,
|
||||
ApiVersion,
|
||||
ApiVersionClass,
|
||||
apiPlan,
|
||||
@@ -236,7 +236,7 @@ export const handleUpdatePlan = createRoute({
|
||||
features,
|
||||
});
|
||||
|
||||
const versionedResponse = applyResponseVersionChanges<ApiPlan>({
|
||||
const versionedResponse = applyResponseVersionChanges<ApiPlanV1>({
|
||||
input: planResponse,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Product,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {
|
||||
type ApiFreeTrialV2,
|
||||
ApiFreeTrialV2Schema,
|
||||
type ApiPlan,
|
||||
ApiPlanSchema,
|
||||
type ApiPlanV1,
|
||||
ApiPlanV1Schema,
|
||||
AttachScenario,
|
||||
type Feature,
|
||||
type FullCustomer,
|
||||
type FullProduct,
|
||||
getProductItemDisplay,
|
||||
itemsToPlanFeatures,
|
||||
itemToBillingInterval,
|
||||
productItemsToPlanItemsV1,
|
||||
productV2ToBasePrice,
|
||||
productV2ToFeatureItems,
|
||||
sortProductItems,
|
||||
@@ -70,7 +70,7 @@ const getTrialAvailable = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert FullProduct (DB format) to Plan API response format
|
||||
* Convert FullProduct (DB format) to Plan API response format (V1/latest)
|
||||
*/
|
||||
export const getPlanResponse = async ({
|
||||
product,
|
||||
@@ -86,7 +86,7 @@ export const getPlanResponse = async ({
|
||||
db?: DrizzleCli;
|
||||
currency?: string;
|
||||
expand?: string[];
|
||||
}): Promise<ApiPlan> => {
|
||||
}): Promise<ApiPlanV1> => {
|
||||
// 1. Convert prices/entitlements to items
|
||||
const rawItems = mapToProductItems({
|
||||
prices: product.prices,
|
||||
@@ -102,7 +102,7 @@ export const getPlanResponse = async ({
|
||||
|
||||
// 4. Extract base price using existing helper
|
||||
const basePriceItem = productV2ToBasePrice({ product: productV2 as any });
|
||||
const basePrice: ApiPlan["price"] | null = basePriceItem
|
||||
const basePrice: ApiPlanV1["price"] | null = basePriceItem
|
||||
? {
|
||||
amount: basePriceItem.price,
|
||||
interval: itemToBillingInterval({ item: basePriceItem }),
|
||||
@@ -125,13 +125,13 @@ export const getPlanResponse = async ({
|
||||
});
|
||||
|
||||
// 6. Convert items to plan features
|
||||
let planFeatures = itemsToPlanFeatures({
|
||||
let planItems = productItemsToPlanItemsV1({
|
||||
items: featureItems,
|
||||
features,
|
||||
expand,
|
||||
});
|
||||
|
||||
planFeatures = planFeatures.map((pf) => ({ ...pf, proration: undefined }));
|
||||
planItems = planItems.map((item) => ({ ...item, proration: undefined }));
|
||||
|
||||
// 7. Get attach scenario for customer context
|
||||
const attachScenario = getAttachScenario({
|
||||
@@ -152,23 +152,23 @@ export const getPlanResponse = async ({
|
||||
});
|
||||
|
||||
// 9. Build Plan response
|
||||
return ApiPlanSchema.parse({
|
||||
return ApiPlanV1Schema.parse({
|
||||
// Basic fields
|
||||
id: product.id,
|
||||
name: product.name || null,
|
||||
description: product.description || null, // Products don't have descriptions
|
||||
name: product.name || "",
|
||||
description: product.description || null,
|
||||
group: product.group || null,
|
||||
version: product.version,
|
||||
|
||||
// Boolean flags
|
||||
add_on: product.is_add_on,
|
||||
default: product.is_default,
|
||||
auto_enable: product.is_default,
|
||||
|
||||
// Price field (optional - only for products with base price)
|
||||
price: basePrice,
|
||||
|
||||
// Features array
|
||||
features: planFeatures ?? [],
|
||||
// Items array (V1 uses "items" not "features")
|
||||
items: planItems ?? [],
|
||||
|
||||
// Free trial
|
||||
free_trial: freeTrial,
|
||||
@@ -186,5 +186,5 @@ export const getPlanResponse = async ({
|
||||
scenario: attachScenario,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
} satisfies ApiPlanV1);
|
||||
};
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { beforeAll, describe, test } from "bun:test";
|
||||
import { type ApiProduct, ApiVersion } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import {
|
||||
constructArrearItem,
|
||||
constructFeatureItem,
|
||||
constructPrepaidItem,
|
||||
} from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
const messagesFeature = constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
});
|
||||
|
||||
const usageFeature = constructArrearItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 10,
|
||||
billingUnits: 1,
|
||||
price: 0.5,
|
||||
});
|
||||
|
||||
const prepaidFeature = constructPrepaidItem({
|
||||
featureId: TestFeature.Credits,
|
||||
billingUnits: 150,
|
||||
price: 10,
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
items: [messagesFeature, usageFeature, prepaidFeature],
|
||||
});
|
||||
|
||||
const testCase = "get-plan1";
|
||||
|
||||
describe(`${chalk.yellowBright("get-plan1: get plan response v1.2")}`, () => {
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
});
|
||||
});
|
||||
|
||||
test("should track version 1.2 response", async () => {
|
||||
const plan = (await autumnV1.products.get(pro.id)) as ApiProduct;
|
||||
|
||||
// 1. Check messages product item
|
||||
const msgesResponseItem = plan.items.find(
|
||||
(item) => item.feature_id === TestFeature.Messages,
|
||||
);
|
||||
|
||||
console.log(msgesResponseItem);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { type ApiProduct, ApiVersion } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { items } from "@tests/utils/fixtures/items.js";
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
|
||||
const testCase = "get-plan-basic";
|
||||
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
||||
const wordsItem = items.consumableWords({ includedUsage: 10 });
|
||||
const creditsItem = items.monthlyCredits({ includedUsage: 10 });
|
||||
|
||||
const pro = products.pro({
|
||||
id: "pro",
|
||||
items: [messagesItem, wordsItem, creditsItem],
|
||||
});
|
||||
|
||||
test.concurrent(`${chalk.yellowBright("get-plan-basic: get plan response v1.2")}`, async () => {
|
||||
await initScenario({
|
||||
setup: [s.products({ list: [pro], prefix: testCase })],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
const productV2 = (await autumnV1.products.get(pro.id)) as ApiProduct;
|
||||
|
||||
// Check messages product item
|
||||
const messagesResponseItem = productV2.items.find(
|
||||
(item) => item.feature_id === TestFeature.Messages,
|
||||
);
|
||||
|
||||
const wordsResponseItem = productV2.items.find(
|
||||
(item) => item.feature_id === TestFeature.Words,
|
||||
);
|
||||
|
||||
const creditsResponseItem = productV2.items.find(
|
||||
(item) => item.feature_id === TestFeature.Credits,
|
||||
);
|
||||
|
||||
const priceItem = productV2.items.find(
|
||||
(item) => item.type === ("price" as const),
|
||||
);
|
||||
|
||||
expect(messagesResponseItem).toBeDefined();
|
||||
expect(wordsResponseItem).toBeDefined();
|
||||
expect(creditsResponseItem).toBeDefined();
|
||||
expect(priceItem).toBeDefined();
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import {
|
||||
type ApiPlan,
|
||||
ApiPlanV0Schema,
|
||||
type ApiPlanV1,
|
||||
ApiPlanV1Schema,
|
||||
type ApiProduct,
|
||||
ApiProductSchema,
|
||||
ApiVersion,
|
||||
} from "@autumn/shared";
|
||||
import { items } from "@tests/utils/fixtures/items.js";
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
|
||||
const testCase = "list-plans-cross-version";
|
||||
|
||||
const creditsItem = items.monthlyCredits({ includedUsage: 500 });
|
||||
const creditsItemPro = items.monthlyCredits({ includedUsage: 5000 });
|
||||
const creditsItemPremium = items.monthlyCredits({ includedUsage: 50_000 });
|
||||
|
||||
const free = products.base({
|
||||
id: "free",
|
||||
isDefault: true,
|
||||
items: [creditsItem],
|
||||
});
|
||||
|
||||
const pro = products.pro({
|
||||
id: "pro",
|
||||
items: [creditsItemPro],
|
||||
});
|
||||
|
||||
const premium = products.premium({
|
||||
id: "premium",
|
||||
items: [creditsItemPremium],
|
||||
});
|
||||
|
||||
test.concurrent(`${chalk.yellowBright("list-plans-cross-version: list products cross version")}`, async () => {
|
||||
await initScenario({
|
||||
setup: [s.products({ list: [free, pro, premium], prefix: testCase })],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
const autumnV2_0 = new AutumnInt({ version: ApiVersion.V2_0 });
|
||||
const autumnV2_1 = new AutumnInt({ version: ApiVersion.V2_1 });
|
||||
|
||||
// V2.1 - should return ApiPlanV1 schema (items, auto_enable)
|
||||
const plansV2_1 = await autumnV2_1.products.list<ApiPlanV1[]>();
|
||||
for (const plan of plansV2_1.list) {
|
||||
ApiPlanV1Schema.parse(plan);
|
||||
}
|
||||
|
||||
// V2.0 - should return ApiPlan schema (features, default)
|
||||
const plansV2_0 = await autumnV2_0.products.list<ApiPlan[]>();
|
||||
for (const plan of plansV2_0.list) {
|
||||
ApiPlanV0Schema.parse(plan);
|
||||
}
|
||||
|
||||
// V1.2 - should return ApiProduct schema
|
||||
const productsV1 = await autumnV1.products.list<ApiProduct[]>();
|
||||
for (const product of productsV1.list) {
|
||||
ApiProductSchema.parse(product);
|
||||
}
|
||||
|
||||
// Verify we have the expected products
|
||||
expect(plansV2_1.list.length).toBeGreaterThanOrEqual(3);
|
||||
expect(plansV2_0.list.length).toBeGreaterThanOrEqual(3);
|
||||
expect(productsV1.list.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { ApiPlanItemV0WithMeta } from "@api/products/items/apiPlanItemV0.js";
|
||||
import { ApiPlanItemV0WithMeta } from "@api/products/items/previousVersions/apiPlanItemV0.js";
|
||||
import yaml from "yaml";
|
||||
import { createDocument } from "zod-openapi";
|
||||
import { CustomerDataSchema } from "../common/customerData.js";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { z } from "zod/v4";
|
||||
import { SuccessResponseSchema } from "../common/commonResponses.js";
|
||||
import { ApiPlanSchema } from "../products/apiPlan.js";
|
||||
import {
|
||||
CreatePlanParamsSchema,
|
||||
ListPlansQuerySchema,
|
||||
UpdatePlanParamsSchema,
|
||||
} from "../products/crud/planOpModels.js";
|
||||
import { ApiPlanV0Schema } from "../products/previousVersions/apiPlanV0.js";
|
||||
|
||||
export const ApiPlanWithMeta = ApiPlanSchema.meta({
|
||||
export const ApiPlanWithMeta = ApiPlanV0Schema.meta({
|
||||
id: "Plan",
|
||||
// examples: [PLAN_EXAMPLE],
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
import { CheckoutParamsV1Schema } from "../checkout/checkoutParamsV1";
|
||||
|
||||
export const AttachBodyV1Schema = CheckoutParamsV1Schema.extend({
|
||||
force_checkout: z.boolean().optional(),
|
||||
});
|
||||
export type AttachBodyV1 = z.infer<typeof AttachBodyV1Schema>;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
import { CustomerDataSchema } from "../../common/customerData.js";
|
||||
import { EntityDataSchema } from "../../common/entityData.js";
|
||||
import { FeatureQuantitySchema } from "../common/featureQuantities.js";
|
||||
import { PlanOverrideSchema } from "../common/planOverride.js";
|
||||
|
||||
export const ExtCheckoutParamsV1Schema = z
|
||||
.object({
|
||||
// Customer / Entity Info
|
||||
customer_id: z.string(),
|
||||
plan_id: z.string(),
|
||||
version: z.number().optional(),
|
||||
|
||||
entity_id: z.string().optional(),
|
||||
customer_data: CustomerDataSchema.optional(),
|
||||
entity_data: EntityDataSchema.optional(),
|
||||
feature_quantities: z.array(FeatureQuantitySchema).optional(),
|
||||
|
||||
success_url: z.string().optional(),
|
||||
checkout_session_params: z.record(z.string(), z.any()).optional(),
|
||||
|
||||
reward: z.string().or(z.array(z.string())).optional(),
|
||||
|
||||
invoice: z.boolean().optional(),
|
||||
invoice_settings: z.object({
|
||||
enable_immediately: z.boolean(),
|
||||
finalize_immediately: z.boolean(),
|
||||
}),
|
||||
|
||||
plan_override: PlanOverrideSchema.optional(),
|
||||
})
|
||||
.meta({
|
||||
description:
|
||||
"Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.",
|
||||
});
|
||||
|
||||
export const CheckoutParamsV1Schema = ExtCheckoutParamsV1Schema.extend({
|
||||
setup_payment: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type CheckoutParamsV1 = z.infer<typeof CheckoutParamsV1Schema>;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { CreatePlanItemParamsV0Schema } from "@api/products/items/crud/createPlanItemV0Params.js";
|
||||
import { z } from "zod/v4";
|
||||
import { ApiFreeTrialV2Schema } from "../../models.js";
|
||||
import { PlanPriceSchema } from "../../products/crud/planOpModels.js";
|
||||
|
||||
export const PlanOverrideSchema = z
|
||||
.object({
|
||||
price: PlanPriceSchema.optional(),
|
||||
features: z.array(CreatePlanItemParamsV0Schema).optional(),
|
||||
free_trial: ApiFreeTrialV2Schema.nullable().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.price && !data.features && !data.free_trial) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Plan override must contain at least one of price, features, or free_trial",
|
||||
},
|
||||
);
|
||||
|
||||
type PlanOverride = z.infer<typeof PlanOverrideSchema>;
|
||||
@@ -1,11 +1,11 @@
|
||||
// Attach
|
||||
export * from "./attach/attachBodyV1.js";
|
||||
|
||||
export * from "./attach/prevVersions/attachBodyV0.js";
|
||||
export * from "./attach/prevVersions/attachResponseV1.js";
|
||||
// Attach V2
|
||||
export * from "./attachV2/attachParamsV0.js";
|
||||
// Checkout
|
||||
export * from "./checkout/checkoutParamsV1.js";
|
||||
|
||||
export * from "./checkout/prevVersions/checkoutParamsV0.js";
|
||||
export * from "./checkout/prevVersions/checkoutResponseV0.js";
|
||||
// Common
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { ApiBaseEntitySchema } from "@api/entities/apiBaseEntity.js";
|
||||
import { ApiCusRewardsSchema } from "@api/others/apiDiscount.js";
|
||||
import { ApiInvoiceV1Schema } from "@api/others/apiInvoice/apiInvoiceV1.js";
|
||||
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||
import { z } from "zod/v4";
|
||||
import { BaseApiCustomerSchema } from "./baseApiCustomer.js";
|
||||
import { ApiCusReferralSchema } from "./components/apiCusReferral.js";
|
||||
import { ApiTrialsUsedV1Schema } from "./components/apiTrialsUsed/apiTrialsUsedV1.js";
|
||||
|
||||
import { ApiBalanceSchema } from "./cusFeatures/apiBalance.js";
|
||||
import { ApiSubscriptionSchema } from "./cusPlans/apiSubscription.js";
|
||||
|
||||
export {
|
||||
type BaseApiCustomer,
|
||||
BaseApiCustomerSchema,
|
||||
} from "./baseApiCustomer.js";
|
||||
|
||||
export const ApiCusExpandSchema = z.object({
|
||||
invoices: z.array(ApiInvoiceV1Schema).optional(),
|
||||
entities: z.array(ApiBaseEntitySchema).optional(),
|
||||
@@ -18,32 +22,17 @@ export const ApiCusExpandSchema = z.object({
|
||||
payment_method: z.any().nullish(),
|
||||
});
|
||||
|
||||
export const BaseApiCustomerSchema = z
|
||||
.object({
|
||||
autumn_id: z.string().optional().meta({
|
||||
internal: true,
|
||||
}),
|
||||
id: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
email: z.string().nullable(),
|
||||
created_at: z.number(),
|
||||
fingerprint: z.string().nullable(),
|
||||
stripe_id: z.string().nullable(),
|
||||
env: z.enum(AppEnv),
|
||||
metadata: z.record(z.any(), z.any()),
|
||||
subscriptions: z.array(ApiSubscriptionSchema),
|
||||
scheduled_subscriptions: z.array(ApiSubscriptionSchema),
|
||||
balances: z.record(z.string(), ApiBalanceSchema),
|
||||
send_email_receipts: z.boolean(),
|
||||
})
|
||||
.meta({
|
||||
id: "BaseCustomer",
|
||||
});
|
||||
// V4 base customer - adds V0 subscriptions and balances
|
||||
export const BaseApiCustomerV4Schema = BaseApiCustomerSchema.extend({
|
||||
subscriptions: z.array(ApiSubscriptionSchema),
|
||||
scheduled_subscriptions: z.array(ApiSubscriptionSchema),
|
||||
balances: z.record(z.string(), ApiBalanceSchema),
|
||||
});
|
||||
|
||||
export const ApiCustomerSchema = BaseApiCustomerSchema.extend(
|
||||
export const ApiCustomerSchema = BaseApiCustomerV4Schema.extend(
|
||||
ApiCusExpandSchema.shape,
|
||||
);
|
||||
|
||||
export type ApiCustomer = z.infer<typeof ApiCustomerSchema>;
|
||||
export type ApiCusExpand = z.infer<typeof ApiCusExpandSchema>;
|
||||
export type BaseApiCustomer = z.infer<typeof BaseApiCustomerSchema>;
|
||||
export type BaseApiCustomerV4 = z.infer<typeof BaseApiCustomerV4Schema>;
|
||||
|
||||
18
shared/api/customers/apiCustomerV5.ts
Normal file
18
shared/api/customers/apiCustomerV5.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod/v4";
|
||||
import { ApiCusExpandSchema } from "./apiCustomer.js";
|
||||
import { BaseApiCustomerSchema } from "./baseApiCustomer.js";
|
||||
import { ApiBalanceSchema } from "./cusFeatures/apiBalance.js";
|
||||
import { ApiSubscriptionV1Schema } from "./cusPlans/apiSubscriptionV1.js";
|
||||
|
||||
// V5 base customer - uses V1 subscriptions (single array with status field)
|
||||
export const BaseApiCustomerV5Schema = BaseApiCustomerSchema.extend({
|
||||
subscriptions: z.array(ApiSubscriptionV1Schema),
|
||||
balances: z.record(z.string(), ApiBalanceSchema),
|
||||
});
|
||||
|
||||
export const ApiCustomerV5Schema = BaseApiCustomerV5Schema.extend(
|
||||
ApiCusExpandSchema.shape,
|
||||
);
|
||||
|
||||
export type ApiCustomerV5 = z.infer<typeof ApiCustomerV5Schema>;
|
||||
export type BaseApiCustomerV5 = z.infer<typeof BaseApiCustomerV5Schema>;
|
||||
19
shared/api/customers/baseApiCustomer.ts
Normal file
19
shared/api/customers/baseApiCustomer.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const BaseApiCustomerSchema = z.object({
|
||||
autumn_id: z.string().optional().meta({
|
||||
internal: true,
|
||||
}),
|
||||
id: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
email: z.string().nullable(),
|
||||
created_at: z.number(),
|
||||
fingerprint: z.string().nullable(),
|
||||
stripe_id: z.string().nullable(),
|
||||
env: z.enum(AppEnv),
|
||||
metadata: z.record(z.any(), z.any()),
|
||||
send_email_receipts: z.boolean(),
|
||||
});
|
||||
|
||||
export type BaseApiCustomer = z.infer<typeof BaseApiCustomerSchema>;
|
||||
49
shared/api/customers/changes/V2.0_CustomerChange.ts
Normal file
49
shared/api/customers/changes/V2.0_CustomerChange.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||
import {
|
||||
AffectedResource,
|
||||
defineVersionChange,
|
||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||
import type { z } from "zod/v4";
|
||||
import { ApiCustomerSchema } from "../apiCustomer.js";
|
||||
import { ApiCustomerV5Schema } from "../apiCustomerV5.js";
|
||||
import type { ApiSubscription } from "../cusPlans/apiSubscription.js";
|
||||
import { apiSubscriptionV1ToV0 } from "../cusPlans/mappers/apiSubscriptionV1ToV0.js";
|
||||
import { CustomerLegacyDataSchema } from "../customerLegacyData.js";
|
||||
|
||||
export const V2_0_CustomerChange = defineVersionChange({
|
||||
name: "V2_0 Customer Change",
|
||||
newVersion: ApiVersion.V2_1,
|
||||
oldVersion: ApiVersion.V2_0,
|
||||
description: [
|
||||
"Subscription schema transforms (V1 to V0) - plan field versioning",
|
||||
],
|
||||
affectedResources: [AffectedResource.Customer],
|
||||
newSchema: ApiCustomerV5Schema,
|
||||
oldSchema: ApiCustomerSchema,
|
||||
legacyDataSchema: CustomerLegacyDataSchema,
|
||||
affectsResponse: true,
|
||||
|
||||
transformResponse: ({
|
||||
input,
|
||||
}: {
|
||||
input: z.infer<typeof ApiCustomerV5Schema>;
|
||||
legacyData?: z.infer<typeof CustomerLegacyDataSchema>;
|
||||
}): z.infer<typeof ApiCustomerSchema> => {
|
||||
// Transform subscriptions from V1 to V0
|
||||
const allSubscriptions = input.subscriptions ?? [];
|
||||
|
||||
const activeSubscriptionsV0: ApiSubscription[] = allSubscriptions
|
||||
.filter((sub) => sub.status === "active")
|
||||
.map((sub) => apiSubscriptionV1ToV0({ input: sub }));
|
||||
|
||||
const scheduledSubscriptionsV0: ApiSubscription[] = allSubscriptions
|
||||
.filter((sub) => sub.status === "scheduled")
|
||||
.map((sub) => apiSubscriptionV1ToV0({ input: sub }));
|
||||
|
||||
return {
|
||||
...input,
|
||||
subscriptions: activeSubscriptionsV0,
|
||||
scheduled_subscriptions: scheduledSubscriptionsV0,
|
||||
};
|
||||
},
|
||||
});
|
||||
4
shared/api/customers/components/index.ts
Normal file
4
shared/api/customers/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./apiCusReferral.js";
|
||||
export * from "./apiCusUpcomingInvoice.js";
|
||||
export * from "./apiTrialsUsed/apiTrialsUsedV1.js";
|
||||
export * from "./apiTrialsUsed/prevVersions/apiTrialsUsedV0.js";
|
||||
1
shared/api/customers/crud/index.ts
Normal file
1
shared/api/customers/crud/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./listCustomersParamsV2.js";
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
defineVersionChange,
|
||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||
import type { EntInterval } from "@models/productModels/intervals/entitlementInterval.js";
|
||||
import { resetIntvToEntIntv } from "@utils/productV2Utils/productItemUtils/convertProductItem/planItemIntervals.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type { z } from "zod/v4";
|
||||
import { FeatureType } from "../../../../models/featureModels/featureEnums.js";
|
||||
import { resetIntvToEntIntv } from "../../../../utils/planFeatureUtils/planFeatureIntervals.js";
|
||||
import { sumValues } from "../../../../utils/utils.js";
|
||||
import type { ApiFeatureV1 } from "../../../features/apiFeatureV1.js";
|
||||
import {
|
||||
|
||||
6
shared/api/customers/cusFeatures/index.ts
Normal file
6
shared/api/customers/cusFeatures/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./apiBalance.js";
|
||||
export * from "./cusFeatureLegacyData.js";
|
||||
export * from "./previousVersions/apiCusFeatureV0.js";
|
||||
export * from "./previousVersions/apiCusFeatureV1.js";
|
||||
export * from "./previousVersions/apiCusFeatureV2.js";
|
||||
export * from "./previousVersions/apiCusFeatureV3.js";
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApiPlanSchema } from "@api/products/apiPlan.js";
|
||||
import { ApiPlanV0Schema } from "@api/products/previousVersions/apiPlanV0";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ApiSubscriptionSchema = z.object({
|
||||
plan: ApiPlanSchema.optional(),
|
||||
plan: ApiPlanV0Schema.optional(),
|
||||
plan_id: z.string(),
|
||||
|
||||
default: z.boolean(),
|
||||
@@ -19,14 +19,6 @@ export const ApiSubscriptionSchema = z.object({
|
||||
current_period_start: z.number().nullable(),
|
||||
current_period_end: z.number().nullable(),
|
||||
quantity: z.number(),
|
||||
|
||||
// feature_quantities: z.array(
|
||||
// z.object({
|
||||
// feature_id: z.string(),
|
||||
// quantity: z.number(),
|
||||
// upcoming_quantity: z.number().nullable(),
|
||||
// }),
|
||||
// ),
|
||||
});
|
||||
|
||||
export type ApiSubscription = z.infer<typeof ApiSubscriptionSchema>;
|
||||
|
||||
32
shared/api/customers/cusPlans/apiSubscriptionV1.ts
Normal file
32
shared/api/customers/cusPlans/apiSubscriptionV1.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ApiPlanV1Schema } from "@api/products/apiPlanV1.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ApiSubscriptionV1Schema = z.object({
|
||||
plan: ApiPlanV1Schema.optional(),
|
||||
plan_id: z.string(),
|
||||
|
||||
auto_enable: z.boolean(),
|
||||
add_on: z.boolean(),
|
||||
|
||||
// Flags / timestamps
|
||||
status: z.enum(["active", "scheduled", "expired"]),
|
||||
past_due: z.boolean(),
|
||||
canceled_at: z.number().nullable(),
|
||||
expires_at: z.number().nullable(),
|
||||
trial_ends_at: z.number().nullable(),
|
||||
|
||||
started_at: z.number(),
|
||||
current_period_start: z.number().nullable(),
|
||||
current_period_end: z.number().nullable(),
|
||||
quantity: z.number(),
|
||||
});
|
||||
|
||||
export const ApiPurchaseV0Schema = z.object({
|
||||
plan: ApiPlanV1Schema.optional(),
|
||||
plan_id: z.string(),
|
||||
expires_at: z.number().nullable(),
|
||||
started_at: z.number(),
|
||||
});
|
||||
|
||||
export type ApiSubscriptionV1 = z.infer<typeof ApiSubscriptionV1Schema>;
|
||||
export type ApiPurchaseV0 = z.infer<typeof ApiPurchaseV0Schema>;
|
||||
@@ -1,9 +1,4 @@
|
||||
import type { ApiProductItemV0Schema } from "@api/products/items/previousVersions/apiProductItemV0.js";
|
||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||
import {
|
||||
AffectedResource,
|
||||
defineVersionChange,
|
||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||
import { TierInfinite } from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import {
|
||||
isFeatureItem,
|
||||
@@ -11,12 +6,9 @@ import {
|
||||
} from "@utils/productV2Utils/productItemUtils/getItemType.js";
|
||||
import { notNullish } from "@utils/utils.js";
|
||||
import type { z } from "zod/v4";
|
||||
import {
|
||||
type CusProductLegacyData,
|
||||
CusProductLegacyDataSchema,
|
||||
} from "../cusProductLegacyData.js";
|
||||
import { ApiCusProductV1Schema } from "../previousVersions/apiCusProductV1.js";
|
||||
import { ApiCusProductV2Schema } from "../previousVersions/apiCusProductV2.js";
|
||||
import type { CusProductLegacyData } from "../cusProductLegacyData.js";
|
||||
import type { ApiCusProductV1Schema } from "../previousVersions/apiCusProductV1.js";
|
||||
import type { ApiCusProductV2Schema } from "../previousVersions/apiCusProductV2.js";
|
||||
|
||||
/**
|
||||
* Transform product from V2 format to V1 format
|
||||
@@ -134,17 +126,3 @@ export function transformCusProductV2ToV1({
|
||||
|
||||
return v1CusProduct;
|
||||
}
|
||||
|
||||
const V0_2_CusProductChange = defineVersionChange({
|
||||
newVersion: ApiVersion.V1_1, // Breaking change introduced in V1_1
|
||||
oldVersion: ApiVersion.V0_2, // Applied when targetVersion <= V0_2
|
||||
description: ["Customer product response revamped to fit ProductV2 schema"],
|
||||
|
||||
affectedResources: [AffectedResource.CusProduct],
|
||||
newSchema: ApiCusProductV2Schema,
|
||||
oldSchema: ApiCusProductV1Schema,
|
||||
legacyDataSchema: CusProductLegacyDataSchema,
|
||||
|
||||
// Response: V1.1+ (V2) → V0_2 (V1)
|
||||
transformResponse: transformCusProductV2ToV1,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { planV1ToV0 } from "@api/products/mappers/planV1ToV0.js";
|
||||
import type { ApiSubscription } from "../apiSubscription.js";
|
||||
import type { ApiSubscriptionV1 } from "../apiSubscriptionV1.js";
|
||||
|
||||
export function transformApiSubscriptionV1ToV0({
|
||||
input,
|
||||
}: {
|
||||
input: ApiSubscriptionV1;
|
||||
}): ApiSubscription {
|
||||
return {
|
||||
plan: input.plan ? planV1ToV0(input.plan) : undefined,
|
||||
plan_id: input.plan_id,
|
||||
default: input.auto_enable,
|
||||
add_on: input.add_on,
|
||||
status: input.status,
|
||||
past_due: input.past_due,
|
||||
canceled_at: input.canceled_at,
|
||||
expires_at: input.expires_at,
|
||||
trial_ends_at: input.trial_ends_at,
|
||||
started_at: input.started_at,
|
||||
current_period_start: input.current_period_start,
|
||||
current_period_end: input.current_period_end,
|
||||
quantity: input.quantity,
|
||||
};
|
||||
}
|
||||
9
shared/api/customers/cusPlans/index.ts
Normal file
9
shared/api/customers/cusPlans/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from "./apiSubscription.js";
|
||||
export * from "./apiSubscriptionV1.js";
|
||||
export * from "./cusProductLegacyData.js";
|
||||
export * from "./mappers/apiSubscriptionV1ToPurchaseV0.js";
|
||||
export * from "./mappers/apiSubscriptionV1ToV0.js";
|
||||
export * from "./previousVersions/apiCusProductV0.js";
|
||||
export * from "./previousVersions/apiCusProductV1.js";
|
||||
export * from "./previousVersions/apiCusProductV2.js";
|
||||
export * from "./previousVersions/apiCusProductV3.js";
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ApiPurchaseV0, ApiSubscriptionV1 } from "../apiSubscriptionV1.js";
|
||||
|
||||
export function apiSubscriptionV1ToPurchaseV0({
|
||||
apiSubscriptionV1,
|
||||
}: {
|
||||
apiSubscriptionV1: ApiSubscriptionV1;
|
||||
}): ApiPurchaseV0 {
|
||||
const input = apiSubscriptionV1;
|
||||
|
||||
return {
|
||||
plan: input.plan,
|
||||
plan_id: input.plan_id,
|
||||
expires_at: input.expires_at,
|
||||
started_at: input.started_at,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { planV1ToV0 } from "@api/products/mappers/planV1ToV0.js";
|
||||
import type { ApiSubscription } from "../apiSubscription.js";
|
||||
import type { ApiSubscriptionV1 } from "../apiSubscriptionV1.js";
|
||||
|
||||
export function apiSubscriptionV1ToV0({
|
||||
input,
|
||||
}: {
|
||||
input: ApiSubscriptionV1;
|
||||
}): ApiSubscription {
|
||||
return {
|
||||
plan: input.plan ? planV1ToV0(input.plan) : undefined,
|
||||
plan_id: input.plan_id,
|
||||
default: input.auto_enable,
|
||||
add_on: input.add_on,
|
||||
status: input.status,
|
||||
past_due: input.past_due,
|
||||
canceled_at: input.canceled_at,
|
||||
expires_at: input.expires_at,
|
||||
trial_ends_at: input.trial_ends_at,
|
||||
started_at: input.started_at,
|
||||
current_period_start: input.current_period_start,
|
||||
current_period_end: input.current_period_end,
|
||||
quantity: input.quantity,
|
||||
};
|
||||
}
|
||||
16
shared/api/customers/index.ts
Normal file
16
shared/api/customers/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Main customer schemas
|
||||
export * from "./apiCustomer.js";
|
||||
export * from "./apiCustomerV5.js";
|
||||
export * from "./baseApiCustomer.js";
|
||||
// Submodules
|
||||
export * from "./components/index.js";
|
||||
export * from "./createCustomerParams.js";
|
||||
export * from "./crud/index.js";
|
||||
export * from "./cusFeatures/index.js";
|
||||
export * from "./cusPlans/index.js";
|
||||
export * from "./customerLegacyData.js";
|
||||
export * from "./customerOpModels.js";
|
||||
export * from "./previousVersions/index.js";
|
||||
|
||||
// NOTE: changes/ and requestChanges/ are NOT exported here to avoid circular imports
|
||||
// Import them directly where needed (e.g., versionChangeRegistry.ts)
|
||||
4
shared/api/customers/previousVersions/index.ts
Normal file
4
shared/api/customers/previousVersions/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./apiCustomerV0.js";
|
||||
export * from "./apiCustomerV1.js";
|
||||
export * from "./apiCustomerV2.js";
|
||||
export * from "./apiCustomerV3.js";
|
||||
25
shared/api/entities/apiEntityV2.ts
Normal file
25
shared/api/entities/apiEntityV2.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod/v4";
|
||||
import { ApiBalanceSchema } from "../customers/cusFeatures/apiBalance.js";
|
||||
import { ApiSubscriptionV1Schema } from "../customers/cusPlans/apiSubscriptionV1.js";
|
||||
import { ApiInvoiceV1Schema } from "../others/apiInvoice/apiInvoiceV1.js";
|
||||
import { ApiBaseEntitySchema } from "./apiBaseEntity.js";
|
||||
|
||||
// V2 base entity - uses V1 subscriptions (single array with status field)
|
||||
export const BaseApiEntityV2Schema = ApiBaseEntitySchema.extend({
|
||||
subscriptions: z.array(ApiSubscriptionV1Schema),
|
||||
balances: z.record(z.string(), ApiBalanceSchema),
|
||||
});
|
||||
|
||||
export const ApiEntityExpandSchema = z.object({
|
||||
invoices: z.array(ApiInvoiceV1Schema).optional().meta({
|
||||
description:
|
||||
"Invoices for this entity (only included when expand=invoices)",
|
||||
}),
|
||||
});
|
||||
|
||||
export const ApiEntityV2Schema = BaseApiEntityV2Schema.extend(
|
||||
ApiEntityExpandSchema.shape,
|
||||
);
|
||||
|
||||
export type ApiEntityV2 = z.infer<typeof ApiEntityV2Schema>;
|
||||
export type BaseApiEntityV2 = z.infer<typeof BaseApiEntityV2Schema>;
|
||||
63
shared/api/entities/changes/V2.0_EntityChange.ts
Normal file
63
shared/api/entities/changes/V2.0_EntityChange.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||
import {
|
||||
AffectedResource,
|
||||
defineVersionChange,
|
||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||
import type { z } from "zod/v4";
|
||||
import type { ApiSubscription } from "../../customers/cusPlans/apiSubscription.js";
|
||||
import { apiSubscriptionV1ToV0 } from "../../customers/cusPlans/mappers/apiSubscriptionV1ToV0.js";
|
||||
import { ApiEntityV1Schema } from "../apiEntity.js";
|
||||
import { ApiEntityV2Schema } from "../apiEntityV2.js";
|
||||
import { EntityLegacyDataSchema } from "../entityLegacyData.js";
|
||||
|
||||
/**
|
||||
* V2.0_EntityChange: Transforms entity response TO V1 format from V2 format
|
||||
*
|
||||
* Applied when: targetVersion <= V2.0 (request is for V2.0 or older)
|
||||
*
|
||||
* Breaking changes introduced in V2.1:
|
||||
*
|
||||
* 1. Subscription schema changes:
|
||||
* - V2.1+: Single "subscriptions" array with ApiSubscriptionV1 (auto_enable, ApiPlanV1)
|
||||
* - V2.0: Split arrays "subscriptions" + "scheduled_subscriptions" with ApiSubscription (default, ApiPlanV0)
|
||||
*
|
||||
* Input: ApiEntityV2 (V2.1+ format)
|
||||
* Output: ApiEntityV1 (V2.0 format)
|
||||
*/
|
||||
export const V2_0_EntityChange = defineVersionChange({
|
||||
name: "V2_0 Entity Change",
|
||||
newVersion: ApiVersion.V2_1,
|
||||
oldVersion: ApiVersion.V2_0,
|
||||
description: [
|
||||
"Subscription schema transforms (V1 to V0) - plan field versioning",
|
||||
],
|
||||
affectedResources: [AffectedResource.Entity],
|
||||
newSchema: ApiEntityV2Schema,
|
||||
oldSchema: ApiEntityV1Schema,
|
||||
legacyDataSchema: EntityLegacyDataSchema,
|
||||
affectsResponse: true,
|
||||
|
||||
transformResponse: ({
|
||||
input,
|
||||
}: {
|
||||
input: z.infer<typeof ApiEntityV2Schema>;
|
||||
legacyData?: z.infer<typeof EntityLegacyDataSchema>;
|
||||
}): z.infer<typeof ApiEntityV1Schema> => {
|
||||
// Transform subscriptions from V1 to V0
|
||||
const allSubscriptions = input.subscriptions ?? [];
|
||||
|
||||
const activeSubscriptionsV0: ApiSubscription[] = allSubscriptions
|
||||
.filter((sub) => sub.status === "active")
|
||||
.map((sub) => apiSubscriptionV1ToV0({ input: sub }));
|
||||
|
||||
const scheduledSubscriptionsV0: ApiSubscription[] = allSubscriptions
|
||||
.filter((sub) => sub.status === "scheduled")
|
||||
.map((sub) => apiSubscriptionV1ToV0({ input: sub }));
|
||||
|
||||
return {
|
||||
...input,
|
||||
subscriptions: activeSubscriptionsV0,
|
||||
scheduled_subscriptions: scheduledSubscriptionsV0,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -2,41 +2,19 @@
|
||||
|
||||
// NOTE: coreOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation
|
||||
export * from "./core/coreOpModels.js";
|
||||
|
||||
// Helpers
|
||||
export * from "./utils/openApiHelpers.js";
|
||||
export * from "./utils/zodToJSDoc.js";
|
||||
|
||||
// Customers
|
||||
|
||||
export * from "./customers/apiCustomer.js";
|
||||
export * from "./customers/components/apiCusReferral.js";
|
||||
export * from "./customers/components/apiCusUpcomingInvoice.js";
|
||||
export * from "./customers/createCustomerParams.js";
|
||||
export * from "./customers/cusFeatures/apiBalance.js";
|
||||
export * from "./customers/cusFeatures/previousVersions/apiCusFeatureV0.js";
|
||||
export * from "./customers/cusFeatures/previousVersions/apiCusFeatureV1.js";
|
||||
export * from "./customers/cusFeatures/previousVersions/apiCusFeatureV2.js";
|
||||
export * from "./customers/cusFeatures/previousVersions/apiCusFeatureV3.js";
|
||||
export * from "./customers/cusPlans/apiSubscription.js";
|
||||
export * from "./customers/cusPlans/cusProductLegacyData.js";
|
||||
export * from "./customers/customerLegacyData.js";
|
||||
export * from "./customers/customerOpModels.js";
|
||||
export * from "./customers/previousVersions/apiCustomerV2.js";
|
||||
export * from "./customers/previousVersions/apiCustomerV3.js";
|
||||
|
||||
export * from "./customers/index.js";
|
||||
// Entities
|
||||
export * from "./entities/apiEntity.js";
|
||||
export * from "./entities/apiEntityV2.js";
|
||||
export * from "./entities/entityLegacyData.js";
|
||||
export * from "./entities/entityOpModels.js";
|
||||
export * from "./entities/prevVersions/apiEntityV0.js";
|
||||
export * from "./errors/classes/featureErrClasses.js";
|
||||
export * from "./errors/codes/featureErrCodes.js";
|
||||
|
||||
// Features
|
||||
export * from "./features/prevVersions/apiFeatureV0.js";
|
||||
export * from "./features/prevVersions/featureV0OpModels.js";
|
||||
|
||||
// Others
|
||||
export * from "./others/apiDiscount.js";
|
||||
export * from "./others/apiInvoice/apiInvoiceV1.js";
|
||||
@@ -45,6 +23,9 @@ export * from "./products/index.js";
|
||||
// Referrals
|
||||
export * from "./referrals/apiReferralCode.js";
|
||||
export * from "./referrals/referralOpModels.js";
|
||||
// Helpers
|
||||
export * from "./utils/openApiHelpers.js";
|
||||
export * from "./utils/zodToJSDoc.js";
|
||||
|
||||
// NOTE: productsOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation
|
||||
|
||||
@@ -70,9 +51,6 @@ export * from "./billing/index.js";
|
||||
export * from "./common/customerData.js";
|
||||
export * from "./common/entityData.js";
|
||||
export * from "./common/pagePaginationSchemas.js";
|
||||
export * from "./customers/crud/listCustomersParamsV2.js";
|
||||
export * from "./customers/cusFeatures/cusFeatureLegacyData.js";
|
||||
export * from "./customers/cusPlans/previousVersions/apiCusProductV3.js";
|
||||
export * from "./entities/apiBaseEntity.js";
|
||||
// Errors
|
||||
export * from "./errors/index.js";
|
||||
|
||||
45
shared/api/products/apiPlanV1.ts
Normal file
45
shared/api/products/apiPlanV1.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { AttachScenario } from "@models/checkModels/checkPreviewModels.js";
|
||||
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||
import { BillingInterval } from "@models/productModels/intervals/billingInterval.js";
|
||||
import { z } from "zod/v4";
|
||||
import { ApiFreeTrialV2Schema } from "./components/apiFreeTrialV2.js";
|
||||
import { DisplaySchema } from "./components/display.js";
|
||||
import { ApiPlanItemV1Schema } from "./items/apiPlanItemV1.js";
|
||||
|
||||
export const ApiPlanV1Schema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
group: z.string().nullable(),
|
||||
|
||||
version: z.number(),
|
||||
add_on: z.boolean(),
|
||||
auto_enable: z.boolean(),
|
||||
|
||||
price: z
|
||||
.object({
|
||||
amount: z.number(),
|
||||
interval: z.enum(BillingInterval),
|
||||
interval_count: z.number().optional(),
|
||||
display: DisplaySchema.optional(),
|
||||
})
|
||||
.nullable(),
|
||||
|
||||
items: z.array(ApiPlanItemV1Schema),
|
||||
free_trial: ApiFreeTrialV2Schema.optional(),
|
||||
|
||||
// Misc
|
||||
created_at: z.number(),
|
||||
env: z.enum(AppEnv),
|
||||
archived: z.boolean(),
|
||||
base_variant_id: z.string().nullable(),
|
||||
|
||||
customer_eligibility: z
|
||||
.object({
|
||||
trial_available: z.boolean().optional(),
|
||||
scenario: z.enum(AttachScenario),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type ApiPlanV1 = z.infer<typeof ApiPlanV1Schema>;
|
||||
@@ -8,8 +8,11 @@ import type { PriceItem } from "@models/productV2Models/productItemModels/priceI
|
||||
import { isPriceItem } from "@utils/index.js";
|
||||
import type { SharedContext } from "../../../types/sharedContext.js";
|
||||
import { productV2ToProperties } from "../../../utils/productV2Utils/productV2ToProperties.js";
|
||||
import { type ApiPlan, ApiPlanSchema } from "../apiPlan.js";
|
||||
import { PlanLegacyDataSchema } from "../planLegacyData.js";
|
||||
import {
|
||||
type ApiPlan,
|
||||
ApiPlanV0Schema,
|
||||
} from "../previousVersions/apiPlanV0.js";
|
||||
import {
|
||||
type ApiProduct,
|
||||
ApiProductSchema,
|
||||
@@ -53,7 +56,7 @@ export const V1_2_ProductChanges = defineVersionChange({
|
||||
"Boolean fields renamed (add_on <- is_add_on, default <- is_default)",
|
||||
],
|
||||
affectedResources: [AffectedResource.Product],
|
||||
newSchema: ApiPlanSchema,
|
||||
newSchema: ApiPlanV0Schema,
|
||||
oldSchema: ApiProductSchema,
|
||||
legacyDataSchema: PlanLegacyDataSchema,
|
||||
|
||||
@@ -74,15 +77,16 @@ export const V1_2_ProductChanges = defineVersionChange({
|
||||
const productItems = planV0ToProductItems({
|
||||
ctx,
|
||||
plan: input,
|
||||
})
|
||||
.filter((x) => {
|
||||
if (isPriceItem(x)) {
|
||||
const y: PriceItem = x as unknown as PriceItem;
|
||||
return y.price > 0;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}).filter((x) => {
|
||||
if (isPriceItem(x)) {
|
||||
const y: PriceItem = x as unknown as PriceItem;
|
||||
return y.price > 0;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(productItems);
|
||||
|
||||
const productV2 = {
|
||||
id: input.id,
|
||||
|
||||
34
shared/api/products/changes/V2.0_PlanChanges.ts
Normal file
34
shared/api/products/changes/V2.0_PlanChanges.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { type ApiPlanV1, ApiPlanV1Schema } from "@api/products/apiPlanV1.js";
|
||||
import { planV1ToV0 } from "@api/products/mappers/planV1ToV0.js";
|
||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||
import {
|
||||
AffectedResource,
|
||||
defineVersionChange,
|
||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||
import { PlanLegacyDataSchema } from "../planLegacyData.js";
|
||||
import {
|
||||
type ApiPlan,
|
||||
ApiPlanV0Schema,
|
||||
} from "../previousVersions/apiPlanV0.js";
|
||||
|
||||
export const V2_0_PlanChanges = defineVersionChange({
|
||||
newVersion: ApiVersion.V2_1,
|
||||
oldVersion: ApiVersion.V2_0,
|
||||
description: [
|
||||
"Plan format changed from V2.0 to V2.1 schema",
|
||||
"Renamed default to auto_enable",
|
||||
"Renamed granted_balance to included",
|
||||
"Removed reset_when_enabled",
|
||||
],
|
||||
affectedResources: [AffectedResource.Product],
|
||||
newSchema: ApiPlanV1Schema,
|
||||
oldSchema: ApiPlanV0Schema,
|
||||
legacyDataSchema: PlanLegacyDataSchema,
|
||||
|
||||
affectsRequest: false,
|
||||
affectsResponse: true,
|
||||
|
||||
transformResponse: ({ input }: { input: ApiPlanV1 }): ApiPlan => {
|
||||
return planV1ToV0(input);
|
||||
},
|
||||
});
|
||||
10
shared/api/products/components/apiFreeTrialV2.ts
Normal file
10
shared/api/products/components/apiFreeTrialV2.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { FreeTrialDuration } from "@models/productModels/freeTrialModels/freeTrialEnums.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ApiFreeTrialV2Schema = z.object({
|
||||
duration_type: z.enum(FreeTrialDuration),
|
||||
duration_length: z.number(),
|
||||
card_required: z.boolean(),
|
||||
});
|
||||
|
||||
export type ApiFreeTrialV2 = z.infer<typeof ApiFreeTrialV2Schema>;
|
||||
4
shared/api/products/components/billingMethod.ts
Normal file
4
shared/api/products/components/billingMethod.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum BillingMethod {
|
||||
Prepaid = "prepaid",
|
||||
UsageBased = "usage_based",
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BillingInterval } from "@models/productModels/intervals/billingInterval.js";
|
||||
import { idRegex } from "@utils/utils.js";
|
||||
import { z } from "zod/v4";
|
||||
import { ApiFreeTrialV2Schema } from "../apiPlan.js";
|
||||
import { CreatePlanItemParamsV0Schema } from "../items/crud/createPlanItemV0Params.js";
|
||||
import { ApiFreeTrialV2Schema } from "../previousVersions/apiPlanV0.js";
|
||||
|
||||
export const PlanPriceSchema = z.object({
|
||||
amount: z.number(),
|
||||
|
||||
@@ -2,12 +2,15 @@ import { planV0ToProductItems } from "@api/products/mappers/planV0ToProductItems
|
||||
import { planV0ToProductV2 } from "@api/products/mappers/planV0ToProductV2.js";
|
||||
|
||||
export * from "./apiFreeTrial.js";
|
||||
export * from "./apiPlan.js";
|
||||
export * from "./apiPlanV1.js";
|
||||
export * from "./components/apiFreeTrialV2.js";
|
||||
export * from "./components/billingMethod.js";
|
||||
export * from "./components/display.js";
|
||||
export * from "./crud/planOpModels.js";
|
||||
export * from "./items/index.js";
|
||||
export * from "./mappers/index.js";
|
||||
export * from "./planLegacyData.js";
|
||||
export * from "./previousVersions/apiPlanV0.js";
|
||||
export * from "./previousVersions/apiProduct.js";
|
||||
export * from "./productOpModels.js";
|
||||
export * from "./productsOpenApi.js";
|
||||
|
||||
106
shared/api/products/items/apiPlanItemV1.ts
Normal file
106
shared/api/products/items/apiPlanItemV1.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { ApiFeatureV0Schema } from "@api/features/prevVersions/apiFeatureV0.js";
|
||||
import { BillingMethod } from "@api/products/components/billingMethod.js";
|
||||
import { DisplaySchema } from "@api/products/components/display.js";
|
||||
import { RolloverExpiryDurationType } from "@models/productModels/durationTypes/rolloverExpiryDurationType.js";
|
||||
import { BillingInterval } from "@models/productModels/intervals/billingInterval.js";
|
||||
import { ResetInterval } from "@models/productModels/intervals/resetInterval.js";
|
||||
import { UsageTierSchema } from "@models/productModels/priceModels/priceConfig/usagePriceConfig.js";
|
||||
import {
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
} from "@models/productV2Models/productItemModels/productItemEnums.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ApiPlanItemV1Schema = z
|
||||
.object({
|
||||
feature_id: z.string(),
|
||||
feature: ApiFeatureV0Schema.optional(),
|
||||
|
||||
included: z.number(),
|
||||
unlimited: z.boolean(),
|
||||
|
||||
reset: z
|
||||
.object({
|
||||
interval: z.enum(ResetInterval),
|
||||
interval_count: z.number().optional(),
|
||||
})
|
||||
.nullable(),
|
||||
|
||||
price: z
|
||||
.object({
|
||||
amount: z.number().optional(),
|
||||
tiers: z.array(UsageTierSchema).optional(),
|
||||
|
||||
interval: z.enum(BillingInterval),
|
||||
interval_count: z.number().optional(),
|
||||
|
||||
billing_units: z.number(),
|
||||
billing_method: z.enum(BillingMethod),
|
||||
max_purchase: z.number().nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
|
||||
display: DisplaySchema.optional(),
|
||||
|
||||
rollover: z
|
||||
.object({
|
||||
max: z.number().nullable(),
|
||||
expiry_duration_type: z.enum(RolloverExpiryDurationType),
|
||||
expiry_duration_length: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
proration: z
|
||||
.object({
|
||||
on_increase: z.enum(OnIncrease).optional(),
|
||||
on_decrease: z.enum(OnDecrease).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.check((ctx) => {
|
||||
const resetInterval = ctx.value.reset?.interval;
|
||||
const priceInterval = ctx.value.price?.interval;
|
||||
|
||||
if (
|
||||
resetInterval &&
|
||||
priceInterval &&
|
||||
String(resetInterval) !== String(priceInterval)
|
||||
) {
|
||||
ctx.issues.push({
|
||||
code: "custom",
|
||||
message: "either pass in reset.interval, or price.interval, not both.",
|
||||
input: ctx.value,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
ctx.value !== undefined &&
|
||||
ctx.value.price !== undefined &&
|
||||
ctx.value.price !== null
|
||||
) {
|
||||
if (
|
||||
ctx.value.price.amount &&
|
||||
ctx.value.price.tiers &&
|
||||
ctx.value.price.tiers.length > 0
|
||||
) {
|
||||
ctx.issues.push({
|
||||
code: "custom",
|
||||
message: "Price amount and tiers are mutually exclusive.",
|
||||
input: ctx.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type ApiPlanItemV1 = z.infer<typeof ApiPlanItemV1Schema>;
|
||||
|
||||
export const ApiPlanItemV1WithMeta = ApiPlanItemV1Schema.meta({
|
||||
id: "PlanFeatureV1",
|
||||
description: "Plan feature object returned by the API (V1/latest)",
|
||||
example: {
|
||||
feature_id: "123",
|
||||
included: 100,
|
||||
unlimited: false,
|
||||
price: null,
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import { planItemV0ToProductItem } from "@api/products/items/mappers/planItemV0ToProductItem.js";
|
||||
|
||||
export * from "./apiPlanItemV0.js";
|
||||
export * from "./apiPlanItemV1.js";
|
||||
export * from "./crud/createPlanItemV0Params.js";
|
||||
export * from "./mappers/planItemV0ToProductItem.js";
|
||||
export * from "./mappers/planItemV1ToV0.js";
|
||||
export * from "./previousVersions/apiPlanItemV0.js";
|
||||
export * from "./previousVersions/apiProductItemV0.js";
|
||||
|
||||
export const apiPlanItem = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApiPlanItemV0 } from "@api/products/items/apiPlanItemV0.js";
|
||||
import type { ApiPlanItemV0 } from "@api/products/items/previousVersions/apiPlanItemV0.js";
|
||||
import type { ProrationConfig } from "@models/productModels/priceModels/priceModels.js";
|
||||
import { Infinite } from "@models/productModels/productEnums.js";
|
||||
import {
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { dbToApiFeatureV1 } from "@utils/featureUtils/apiFeatureToDbFeature.js";
|
||||
import { featureToItemFeatureType } from "@utils/featureUtils/convertFeatureUtils.js";
|
||||
|
||||
import { resetIntvToItemIntv } from "@utils/planFeatureUtils/planFeatureIntervals.js";
|
||||
import { resetIntvToItemIntv } from "@utils/productV2Utils/productItemUtils/convertProductItem/planItemIntervals.js";
|
||||
import { billingToItemInterval } from "@utils/productV2Utils/productItemUtils/itemIntervalUtils.js";
|
||||
import type { SharedContext } from "../../../../types/sharedContext.js";
|
||||
import {
|
||||
|
||||
40
shared/api/products/items/mappers/planItemV1ToV0.ts
Normal file
40
shared/api/products/items/mappers/planItemV1ToV0.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { BillingMethod } from "@api/products/components/billingMethod.js";
|
||||
import type { ApiPlanItemV0 } from "@api/products/items/previousVersions/apiPlanItemV0.js";
|
||||
import { UsageModel } from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import type { ApiPlanItemV1 } from "../apiPlanItemV1.js";
|
||||
|
||||
/** Convert billing_method (V1) to usage_model (V0) */
|
||||
export function billingMethodToUsageModel(
|
||||
billingMethod: BillingMethod,
|
||||
): UsageModel {
|
||||
return billingMethod === BillingMethod.Prepaid
|
||||
? UsageModel.Prepaid
|
||||
: UsageModel.PayPerUse;
|
||||
}
|
||||
|
||||
/** Transform ApiPlanItemV1 to ApiPlanItemV0 */
|
||||
export function planItemV1ToV0(item: ApiPlanItemV1): ApiPlanItemV0 {
|
||||
const { included, price, ...restItem } = item;
|
||||
return {
|
||||
...restItem,
|
||||
granted_balance: included,
|
||||
reset: item.reset
|
||||
? {
|
||||
interval: item.reset.interval,
|
||||
interval_count: item.reset.interval_count,
|
||||
reset_when_enabled: false,
|
||||
}
|
||||
: null,
|
||||
price: price
|
||||
? {
|
||||
amount: price.amount,
|
||||
tiers: price.tiers,
|
||||
interval: price.interval,
|
||||
interval_count: price.interval_count,
|
||||
billing_units: price.billing_units,
|
||||
usage_model: billingMethodToUsageModel(price.billing_method),
|
||||
max_purchase: price.max_purchase,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ApiFeatureV0Schema } from "@api/features/prevVersions/apiFeatureV0.js";
|
||||
import { DisplaySchema } from "@api/products/components/display.js";
|
||||
import { RolloverExpiryDurationType } from "@models/productModels/durationTypes/rolloverExpiryDurationType.js";
|
||||
import { BillingInterval } from "@models/productModels/intervals/billingInterval.js";
|
||||
import { ResetInterval } from "@models/productModels/intervals/resetInterval.js";
|
||||
import { UsageTierSchema } from "@models/productModels/priceModels/priceConfig/usagePriceConfig.js";
|
||||
import { UsageModel } from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { z } from "zod/v4";
|
||||
import { RolloverExpiryDurationType } from "../../../models/productModels/durationTypes/rolloverExpiryDurationType.js";
|
||||
import { BillingInterval } from "../../../models/productModels/intervals/billingInterval.js";
|
||||
import {
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
} from "../../../models/productV2Models/productItemModels/productItemEnums.js";
|
||||
import { ApiFeatureV0Schema } from "../../features/prevVersions/apiFeatureV0.js";
|
||||
import { DisplaySchema } from "../components/display.js";
|
||||
} from "@models/productV2Models/productItemModels/productItemEnums.js";
|
||||
import { UsageModel } from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ApiPlanItemV0Schema = z
|
||||
.object({
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ApiPlanItemV0 } from "@api/products/items/apiPlanItemV0.js";
|
||||
import type { CreatePlanItemParamsV0 } from "@api/products/items/crud/createPlanItemV0Params.js";
|
||||
import type { ApiPlanItemV0 } from "@api/products/items/previousVersions/apiPlanItemV0.js";
|
||||
import { notNullish } from "@utils/utils.js";
|
||||
|
||||
type PlanFeatureWithReset = (ApiPlanItemV0 | CreatePlanItemParamsV0) & {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./planV0ToProductItems.js";
|
||||
export * from "./planV1ToV0.js";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ApiPlan } from "@api/products/apiPlan";
|
||||
import type {
|
||||
CreatePlanParams,
|
||||
UpdatePlanParams,
|
||||
} from "@api/products/crud/planOpModels";
|
||||
import type { ApiPlan } from "@api/products/previousVersions/apiPlanV0";
|
||||
import { BillingInterval } from "@models/productModels/intervals/billingInterval";
|
||||
import {
|
||||
type ProductItem,
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
type CreatePlanParams,
|
||||
type UpdatePlanParams,
|
||||
} from "@api/models";
|
||||
import type { ApiPlan } from "@api/products/apiPlan";
|
||||
import { planV0ToBasePriceProductItem } from "@api/products/mappers/planV0ToBasePriceProductItem";
|
||||
import type { ApiPlan } from "@api/products/previousVersions/apiPlanV0";
|
||||
import type { ProductItem } from "@models/productV2Models/productItemModels/productItemModels";
|
||||
import type { SharedContext } from "../../../types";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ApiPlan } from "@api/products/apiPlan";
|
||||
import type {
|
||||
CreatePlanParams,
|
||||
UpdatePlanParams,
|
||||
} from "@api/products/crud/planOpModels";
|
||||
import { planV0ToProductItems } from "@api/products/mappers/planV0ToProductItems";
|
||||
import type { ApiPlan } from "@api/products/previousVersions/apiPlanV0";
|
||||
import type {
|
||||
CreateProductV2Params,
|
||||
UpdateProductV2Params,
|
||||
|
||||
18
shared/api/products/mappers/planV1ToV0.ts
Normal file
18
shared/api/products/mappers/planV1ToV0.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { ApiPlanV1 } from "@api/products/apiPlanV1.js";
|
||||
import { planItemV1ToV0 } from "@api/products/items/mappers/planItemV1ToV0.js";
|
||||
import type { ApiPlan } from "@api/products/previousVersions/apiPlanV0.js";
|
||||
|
||||
/**
|
||||
* Transform ApiPlanV1 to ApiPlan (V0)
|
||||
*
|
||||
* Handles the following conversions:
|
||||
* - auto_enable -> default
|
||||
* - features: ApiPlanItemV1[] -> ApiPlanItemV0[]
|
||||
*/
|
||||
export function planV1ToV0(plan: ApiPlanV1): ApiPlan {
|
||||
return {
|
||||
...plan,
|
||||
default: plan.auto_enable,
|
||||
features: plan.items.map(planItemV1ToV0),
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
import { AttachScenario } from "@models/checkModels/checkPreviewModels.js";
|
||||
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||
import { FreeTrialDuration } from "@models/productModels/freeTrialModels/freeTrialEnums.js";
|
||||
import { BillingInterval } from "@models/productModels/intervals/billingInterval.js";
|
||||
import { z } from "zod/v4";
|
||||
import { DisplaySchema } from "./components/display.js";
|
||||
import { ApiPlanItemV0Schema } from "./items/apiPlanItemV0.js";
|
||||
import { ApiFreeTrialV2Schema } from "../components/apiFreeTrialV2.js";
|
||||
import { DisplaySchema } from "../components/display.js";
|
||||
import { ApiPlanItemV0Schema } from "../items/previousVersions/apiPlanItemV0.js";
|
||||
|
||||
export const ApiFreeTrialV2Schema = z.object({
|
||||
duration_type: z.enum(FreeTrialDuration),
|
||||
duration_length: z.number(),
|
||||
card_required: z.boolean(),
|
||||
});
|
||||
export {
|
||||
type ApiFreeTrialV2,
|
||||
ApiFreeTrialV2Schema,
|
||||
} from "../components/apiFreeTrialV2.js";
|
||||
|
||||
export type ApiFreeTrialV2 = z.infer<typeof ApiFreeTrialV2Schema>;
|
||||
|
||||
export const ApiPlanSchema = z.object({
|
||||
export const ApiPlanV0Schema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
@@ -51,4 +48,4 @@ export const ApiPlanSchema = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type ApiPlan = z.infer<typeof ApiPlanSchema>;
|
||||
export type ApiPlan = z.infer<typeof ApiPlanV0Schema>;
|
||||
@@ -4,6 +4,7 @@
|
||||
* Internally we use SemVer for comparison (e.g., "1.1.0")
|
||||
*/
|
||||
export enum ApiVersion {
|
||||
V2_1 = "2.1.0",
|
||||
V2_0 = "2.0.0",
|
||||
V1_Beta = "beta",
|
||||
V1_2 = "1.2.0",
|
||||
@@ -16,4 +17,4 @@ export type ApiVersionString = `${ApiVersion}`;
|
||||
|
||||
export const API_VERSIONS = Object.values(ApiVersion);
|
||||
|
||||
export const LATEST_VERSION = ApiVersion.V2_0;
|
||||
export const LATEST_VERSION = ApiVersion.V2_1;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { V1_2_CustomerChange } from "@api/customers/changes/V1.2_CustomerChange.
|
||||
import { V1_2_CustomerQueryChange } from "@api/customers/requestChanges/V1.2_CustomerQueryChange.js";
|
||||
// Import entity changes
|
||||
import { V1_2_EntityChange } from "@api/entities/changes/V1.2_EntityChange.js";
|
||||
import { V2_0_EntityChange } from "@api/entities/changes/V2.0_EntityChange.js";
|
||||
import { V1_2_EntityQueryChange } from "@api/entities/requestChanges/V1.2_EntityQueryChange.js";
|
||||
// Import feature changes
|
||||
import { V1_2_FeatureChange } from "@api/features/changes/V1.2_FeatureChange.js";
|
||||
@@ -22,7 +23,9 @@ import { V1_2_InvoiceChange } from "@api/others/apiInvoice/changes/V1.2_InvoiceC
|
||||
|
||||
// Import product changes
|
||||
|
||||
import { V2_0_CustomerChange } from "@api/customers/changes/V2.0_CustomerChange.js";
|
||||
import { V1_2_ProductChanges } from "@api/products/changes/V1.2_ProductChanges.js";
|
||||
import { V2_0_PlanChanges } from "@api/products/changes/V2.0_PlanChanges.js";
|
||||
import { V0_2_CheckChange } from "../../balances/check/changes/V0.2_CheckChange.js";
|
||||
import { V1_2_CheckChange } from "../../balances/check/changes/V1.2_CheckChange.js";
|
||||
import { V1_2_CheckQueryChange } from "../../balances/check/changes/V1.2_CheckQueryChange.js";
|
||||
@@ -34,6 +37,12 @@ import { ApiVersion } from "../ApiVersion.js";
|
||||
import type { VersionChangeConstructor } from "./VersionChange.js";
|
||||
import { VersionChangeRegistryClass } from "./VersionChangeRegistryClass.js";
|
||||
|
||||
export const V2_1_CHANGES: VersionChangeConstructor[] = [
|
||||
V2_0_PlanChanges, // Transforms Plan TO V2.1 format from V2.0 format
|
||||
V2_0_CustomerChange, // Transforms Customer TO V2.1 format from V2.0 format
|
||||
V2_0_EntityChange, // Transforms Entity TO V2.1 format from V2.0 format
|
||||
];
|
||||
|
||||
export const V2_CHANGES: VersionChangeConstructor[] = [
|
||||
V1_2_CustomerChange, // Transforms Customer TO V1.2 format from V2 format
|
||||
V1_2_CustomerQueryChange, // Transforms Customer Query TO V2.0 format (adds expand options)
|
||||
@@ -73,6 +82,10 @@ export const V0_2_CHANGES: VersionChangeConstructor[] = [
|
||||
export const V0_1_CHANGES: VersionChangeConstructor[] = [];
|
||||
|
||||
export function registerAllVersionChanges() {
|
||||
VersionChangeRegistryClass.register({
|
||||
version: ApiVersion.V2_1,
|
||||
changes: V2_1_CHANGES,
|
||||
});
|
||||
VersionChangeRegistryClass.register({
|
||||
version: ApiVersion.V2_0,
|
||||
changes: V2_CHANGES,
|
||||
|
||||
@@ -14,6 +14,12 @@ export interface VersionMetadata {
|
||||
* SemVer ↔ CalVer mappings and metadata
|
||||
*/
|
||||
export const VERSION_REGISTRY: Record<ApiVersion, VersionMetadata> = {
|
||||
[ApiVersion.V2_1]: {
|
||||
semver: ApiVersion.V2_1,
|
||||
calver: "2026-04-01",
|
||||
releasedAt: new Date("2026-04-01").getTime(),
|
||||
description: "Plan items with max_purchase",
|
||||
},
|
||||
[ApiVersion.V2_0]: {
|
||||
semver: ApiVersion.V2_0,
|
||||
calver: "2026-03-31",
|
||||
|
||||
@@ -86,7 +86,7 @@ export * from "./models/featureModels/featureModels.js";
|
||||
// export * from "./models/featureModels/featureResModels.js";
|
||||
|
||||
export * from "./api/products/crud/planOpModels.js";
|
||||
export * from "./api/products/items/apiPlanItemV0.js";
|
||||
export * from "./api/products/items/previousVersions/apiPlanItemV0.js";
|
||||
|
||||
// 2. Feature Models
|
||||
export * from "./models/featureModels/featureTable.js";
|
||||
@@ -189,12 +189,7 @@ export * from "./utils/cusEntUtils/index";
|
||||
export * from "./utils/displayUtils.js";
|
||||
export * from "./utils/index.js";
|
||||
export * from "./utils/intervalUtils.js";
|
||||
export * from "./utils/planFeatureUtils/itemsToPlanFeatures.js";
|
||||
export * from "./utils/planFeatureUtils/itemsToPlanFeatures.js";
|
||||
export * from "./utils/planFeatureUtils/planFeatureIntervals.js";
|
||||
export * from "./utils/planFeatureUtils/planFeatureIntervals.js";
|
||||
export * from "./utils/planFeatureUtils/planToDbFreeTrial.js";
|
||||
|
||||
export * from "./utils/productDisplayUtils/sortProductItems.js";
|
||||
export * from "./utils/productDisplayUtils.js";
|
||||
export * from "./utils/productUtils/convertProductUtils.js";
|
||||
@@ -209,6 +204,8 @@ export * from "./utils/productV2Utils/compareProductUtils/generateTrialChanges.j
|
||||
export * from "./utils/productV2Utils/compareProductUtils/generateVersionChanges.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils/itemEditTypes.js";
|
||||
export * from "./utils/productV2Utils/productItemUtils/convertItemUtils.js";
|
||||
export * from "./utils/productV2Utils/productItemUtils/convertProductItem/planItemIntervals.js";
|
||||
export * from "./utils/productV2Utils/productItemUtils/convertProductItem/productItemToPlanItemV1.js";
|
||||
export * from "./utils/productV2Utils/productItemUtils/getProductItemRes.js";
|
||||
export * from "./utils/productV2Utils/productItemUtils/itemIntervalUtils.js";
|
||||
export * from "./utils/productV3Utils/productItemUtils/productV3ItemUtils.js";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ApiPlanV1Schema } from "@api/products/apiPlanV1.js";
|
||||
import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js";
|
||||
import { z } from "zod/v4";
|
||||
import { BillingPreviewResponseSchema } from "../../api/billing/common/billingPreviewResponse.js";
|
||||
import { ApiBalanceSchema } from "../../api/customers/cusFeatures/apiBalance.js";
|
||||
import { ApiSubscriptionSchema } from "../../api/customers/cusPlans/apiSubscription.js";
|
||||
import { ApiPlanSchema } from "../../api/products/apiPlan.js";
|
||||
|
||||
/**
|
||||
* Org branding for checkout display
|
||||
@@ -30,18 +29,18 @@ export const CheckoutEntitySchema = z.object({
|
||||
name: z.string().nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Subscription with required plan (always expanded for checkout)
|
||||
*/
|
||||
export const CheckoutSubscriptionSchema = ApiSubscriptionSchema.extend({
|
||||
plan: ApiPlanSchema,
|
||||
});
|
||||
// /**
|
||||
// * Subscription with required plan (always expanded for checkout)
|
||||
// */
|
||||
// export const CheckoutSubscriptionSchema = ApiSubscriptionSchema.extend({
|
||||
// plan: ApiPlanV0Schema,
|
||||
// });
|
||||
|
||||
/**
|
||||
* A change in the checkout (product being added, canceled, or expiring)
|
||||
*/
|
||||
export const CheckoutChangeSchema = z.object({
|
||||
plan: ApiPlanSchema,
|
||||
plan: ApiPlanV1Schema,
|
||||
feature_quantities: z.array(
|
||||
FeatureOptionsSchema.pick({
|
||||
feature_id: true,
|
||||
@@ -80,7 +79,7 @@ export const ConfirmCheckoutResponseSchema = z.object({
|
||||
export type CheckoutOrg = z.infer<typeof CheckoutOrgSchema>;
|
||||
export type CheckoutCustomer = z.infer<typeof CheckoutCustomerSchema>;
|
||||
export type CheckoutEntity = z.infer<typeof CheckoutEntitySchema>;
|
||||
export type CheckoutSubscription = z.infer<typeof CheckoutSubscriptionSchema>;
|
||||
// export type CheckoutSubscription = z.infer<typeof CheckoutSubscriptionSchema>;
|
||||
export type CheckoutChange = z.infer<typeof CheckoutChangeSchema>;
|
||||
export type GetCheckoutResponse = z.infer<typeof GetCheckoutResponseSchema>;
|
||||
export type ConfirmCheckoutResponse = z.infer<
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isContUseFeature } from "../../featureUtils/convertFeatureUtils";
|
||||
import {
|
||||
entIntvToResetIntv,
|
||||
toIntervalCountResponse,
|
||||
} from "../../planFeatureUtils/planFeatureIntervals";
|
||||
} from "../../productV2Utils/productItemUtils/convertProductItem/planItemIntervals.js";
|
||||
|
||||
export const cusEntsToNextResetAt = ({
|
||||
cusEnts,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Decimal } from "decimal.js";
|
||||
import type { ApiBalanceBreakdown } from "../../api/customers/cusFeatures/apiBalance.js";
|
||||
import type { FullCustomerEntitlement } from "../../models/cusProductModels/cusEntModels/cusEntModels.js";
|
||||
import type { FullCusEntWithFullCusProduct } from "../../models/cusProductModels/cusEntModels/cusEntWithProduct.js";
|
||||
import { resetIntvToEntIntv } from "../planFeatureUtils/planFeatureIntervals.js";
|
||||
import { entToOptions } from "../productUtils/convertProductUtils.js";
|
||||
import { resetIntvToEntIntv } from "../productV2Utils/productItemUtils/convertProductItem/planItemIntervals.js";
|
||||
import { getCusEntBalance } from "./balanceUtils.js";
|
||||
import { cusEntToCusPrice } from "./convertCusEntUtils/cusEntToCusPrice.js";
|
||||
import { getRolloverFields } from "./getRolloverFields.js";
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
import {
|
||||
type ApiPlanItemV0,
|
||||
ApiPlanItemV0Schema,
|
||||
} from "@api/products/items/apiPlanItemV0.js";
|
||||
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
||||
import { Infinite } from "@models/productModels/productEnums.js";
|
||||
import {
|
||||
type ProductItem,
|
||||
UsageModel,
|
||||
} from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { InternalError } from "../../api/models.js";
|
||||
import type { Feature } from "../../models/featureModels/featureModels.js";
|
||||
import { expandIncludes } from "../expandUtils.js";
|
||||
import {
|
||||
isBooleanFeature,
|
||||
isContUseFeature,
|
||||
} from "../featureUtils/convertFeatureUtils.js";
|
||||
import { toApiFeature } from "../featureUtils.js";
|
||||
import { getProductItemDisplay } from "../productDisplayUtils.js";
|
||||
import { isFeaturePriceItem } from "../productV2Utils/productItemUtils/getItemType.js";
|
||||
import { itemToBillingInterval } from "../productV2Utils/productItemUtils/itemIntervalUtils.js";
|
||||
import { itemIntvToResetIntv } from "./planFeatureIntervals.js";
|
||||
|
||||
// const getFeaturePriceItemParams = ({
|
||||
// item,
|
||||
// feature,
|
||||
// }: {
|
||||
// item: ProductItem;
|
||||
// feature: Feature;
|
||||
// }) => {
|
||||
|
||||
// // 1. If
|
||||
// // reset_interval: itemIntvToResetIntv(
|
||||
// // item.interval!,
|
||||
// // ) as ResetInterval,
|
||||
// // ...(item.interval_count !== undefined &&
|
||||
// // item.interval_count !== null
|
||||
// // ? {
|
||||
// // reset_interval_count: item.interval_count,
|
||||
// // }
|
||||
// // : {}),
|
||||
// return {
|
||||
// interval: item.interval,
|
||||
// };
|
||||
// };
|
||||
|
||||
const itemToReset = ({
|
||||
item,
|
||||
feature,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
feature: Feature;
|
||||
}) => {
|
||||
// 1. If continuous use or boolean, no reset
|
||||
if (isContUseFeature({ feature }) || isBooleanFeature({ feature })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
interval: itemIntvToResetIntv(item.interval ?? null),
|
||||
interval_count:
|
||||
item.interval_count !== 1 && typeof item.interval_count === "number"
|
||||
? item.interval_count
|
||||
: undefined,
|
||||
reset_when_enabled: item.reset_usage_when_enabled ?? false,
|
||||
} satisfies ApiPlanItemV0["reset"];
|
||||
};
|
||||
|
||||
const itemToPlanFeaturePrice = ({ item }: { item: ProductItem }) => {
|
||||
if (!isFeaturePriceItem(item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const includedUsage =
|
||||
item.included_usage === Infinite ? 0 : (item.included_usage ?? 0);
|
||||
const maxPurchase = item.usage_limit
|
||||
? item.usage_limit - includedUsage
|
||||
: null;
|
||||
|
||||
const price =
|
||||
item.tiers && item.tiers.length === 1 ? item.tiers[0].amount : item.price;
|
||||
|
||||
const tiers =
|
||||
item.tiers && item.tiers.length > 1
|
||||
? item.tiers.map((tier) => ({
|
||||
to: tier.to,
|
||||
amount: tier.amount,
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
amount: price ?? undefined,
|
||||
tiers: tiers,
|
||||
|
||||
interval: itemToBillingInterval({ item }),
|
||||
interval_count:
|
||||
item.interval_count !== 1 && typeof item.interval_count === "number"
|
||||
? item.interval_count
|
||||
: undefined,
|
||||
|
||||
billing_units: item.billing_units ?? 1,
|
||||
usage_model: item.usage_model || UsageModel.PayPerUse,
|
||||
max_purchase: maxPurchase,
|
||||
} satisfies ApiPlanItemV0["price"];
|
||||
};
|
||||
|
||||
const itemToPlanFeatureRollover = ({ item }: { item: ProductItem }) => {
|
||||
if (!item.config?.rollover) return undefined;
|
||||
|
||||
return {
|
||||
max: item.config.rollover.max ?? null,
|
||||
expiry_duration_type: item.config.rollover.duration,
|
||||
expiry_duration_length: item.config.rollover.length,
|
||||
} satisfies ApiPlanItemV0["rollover"];
|
||||
};
|
||||
|
||||
const itemToPlanFeatureProration = ({ item }: { item: ProductItem }) => {
|
||||
if (!item.config?.on_increase || !item.config?.on_decrease) return undefined;
|
||||
|
||||
if (!isFeaturePriceItem(item)) return undefined;
|
||||
|
||||
return {
|
||||
on_increase: item.config.on_increase,
|
||||
on_decrease: item.config.on_decrease,
|
||||
} satisfies ApiPlanItemV0["proration"];
|
||||
};
|
||||
|
||||
export const itemsToPlanFeatures = ({
|
||||
items,
|
||||
features,
|
||||
expand = [],
|
||||
}: {
|
||||
items: ProductItem[];
|
||||
features: Feature[];
|
||||
expand?: string[];
|
||||
}): ApiPlanItemV0[] => {
|
||||
if (!items) return [];
|
||||
|
||||
const shouldExpandFeature = expandIncludes({
|
||||
expand,
|
||||
includes: [CusExpand.PlanFeaturesFeature],
|
||||
});
|
||||
|
||||
return items.map((item) => {
|
||||
const feature = features.find((f) => f.id === item.feature_id);
|
||||
if (!item.feature_id || !feature) {
|
||||
throw new InternalError({
|
||||
message:
|
||||
"Converting item to plan feature: item has no feature ID or feature not found",
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Granted balance
|
||||
const grantedBalance =
|
||||
item.included_usage === Infinite ? 0 : (item.included_usage ?? 0);
|
||||
|
||||
const reset = itemToReset({ item, feature });
|
||||
const price = itemToPlanFeaturePrice({ item });
|
||||
const rollover = itemToPlanFeatureRollover({ item });
|
||||
const proration = itemToPlanFeatureProration({ item });
|
||||
|
||||
// Convert feature to API format if expand requested
|
||||
const apiFeature = shouldExpandFeature
|
||||
? toApiFeature({ feature })
|
||||
: undefined;
|
||||
|
||||
return ApiPlanItemV0Schema.parse({
|
||||
feature_id: item.feature_id,
|
||||
feature: apiFeature,
|
||||
granted_balance: grantedBalance,
|
||||
unlimited: item.included_usage === Infinite,
|
||||
|
||||
reset,
|
||||
price,
|
||||
|
||||
rollover,
|
||||
proration,
|
||||
|
||||
display: getProductItemDisplay({ item, features }),
|
||||
|
||||
// Other fields
|
||||
// entity_feature_id: item.entity_feature_id,
|
||||
} satisfies ApiPlanItemV0);
|
||||
});
|
||||
};
|
||||
|
||||
// // Conditionally set reset_interval OR price.interval (mutually exclusive)
|
||||
// // If has pricing: interval goes in price
|
||||
// // If no pricing: interval goes in reset_interval
|
||||
// ...(!hasPrice && item.interval
|
||||
// ? {
|
||||
// reset_interval: itemIntvToResetIntv(
|
||||
// item.interval!,
|
||||
// ) as ResetInterval,
|
||||
// ...(item.interval_count !== undefined &&
|
||||
// item.interval_count !== null
|
||||
// ? {
|
||||
// reset_interval_count: item.interval_count,
|
||||
// }
|
||||
// : {}),
|
||||
// }
|
||||
// : {}),
|
||||
|
||||
// ...(hasPrice
|
||||
// ? (() => {
|
||||
// // Check if this is a single tier to infinity (stored as tier but should be flat amount)
|
||||
// const isSingleTierToInf =
|
||||
// item.tiers &&
|
||||
// item.tiers.length === 1 &&
|
||||
// item.tiers[0].to === TierInfinite;
|
||||
|
||||
// return {
|
||||
// price: {
|
||||
// interval: (item.interval || "month") as BillingInterval,
|
||||
// billing_units: item.billing_units ?? 1,
|
||||
// usage_model: (item.usage_model || "pay_per_use") as UsageModel,
|
||||
// max_purchase: 1,
|
||||
// // If single tier to infinity, extract amount; otherwise use item.price
|
||||
// amount: isSingleTierToInf
|
||||
// ? item.tiers![0].amount
|
||||
// : item.price || 0,
|
||||
// // Only include tiers if multi-tier
|
||||
// ...(item.tiers && item.tiers.length > 1
|
||||
// ? {
|
||||
// tiers: item.tiers.map((tier) => ({
|
||||
// to: tier.to === TierInfinite ? TierInfinite : tier.to,
|
||||
// amount: tier.amount,
|
||||
// })),
|
||||
// }
|
||||
// : {}),
|
||||
// interval_count: item.interval_count ?? undefined,
|
||||
// },
|
||||
// };
|
||||
// })()
|
||||
// : {}),
|
||||
@@ -0,0 +1,178 @@
|
||||
import { BillingMethod } from "@api/products/components/billingMethod.js";
|
||||
import {
|
||||
type ApiPlanItemV1,
|
||||
ApiPlanItemV1Schema,
|
||||
} from "@api/products/items/apiPlanItemV1.js";
|
||||
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
||||
import { Infinite } from "@models/productModels/productEnums.js";
|
||||
import {
|
||||
type ProductItem,
|
||||
UsageModel,
|
||||
} from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { InternalError } from "../../../../api/models.js";
|
||||
import type { Feature } from "../../../../models/featureModels/featureModels.js";
|
||||
import { expandIncludes } from "../../../expandUtils.js";
|
||||
import {
|
||||
isBooleanFeature,
|
||||
isContUseFeature,
|
||||
} from "../../../featureUtils/convertFeatureUtils.js";
|
||||
import { toApiFeature } from "../../../featureUtils.js";
|
||||
import { getProductItemDisplay } from "../../../productDisplayUtils.js";
|
||||
import { isFeaturePriceItem } from "../getItemType.js";
|
||||
import { itemToBillingInterval } from "../itemIntervalUtils.js";
|
||||
import { itemIntvToResetIntv } from "./planItemIntervals.js";
|
||||
|
||||
const itemToReset = ({
|
||||
item,
|
||||
feature,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
feature: Feature;
|
||||
}): ApiPlanItemV1["reset"] => {
|
||||
// 1. If continuous use or boolean, no reset
|
||||
if (isContUseFeature({ feature }) || isBooleanFeature({ feature })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
interval: itemIntvToResetIntv(item.interval ?? null),
|
||||
interval_count:
|
||||
item.interval_count !== 1 && typeof item.interval_count === "number"
|
||||
? item.interval_count
|
||||
: undefined,
|
||||
// Note: reset_when_enabled is NOT in V1 schema - removed
|
||||
};
|
||||
};
|
||||
|
||||
const itemToPlanFeaturePrice = ({
|
||||
item,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
}): ApiPlanItemV1["price"] => {
|
||||
if (!isFeaturePriceItem(item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const includedUsage =
|
||||
item.included_usage === Infinite ? 0 : (item.included_usage ?? 0);
|
||||
const maxPurchase = item.usage_limit
|
||||
? item.usage_limit - includedUsage
|
||||
: null;
|
||||
|
||||
const price =
|
||||
item.tiers && item.tiers.length === 1 ? item.tiers[0].amount : item.price;
|
||||
|
||||
const tiers =
|
||||
item.tiers && item.tiers.length > 1
|
||||
? item.tiers.map((tier) => ({
|
||||
to: tier.to,
|
||||
amount: tier.amount,
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
// V1 schema uses billing_method, NOT usage_model
|
||||
const billingMethod =
|
||||
item.usage_model === UsageModel.PayPerUse
|
||||
? BillingMethod.UsageBased
|
||||
: BillingMethod.Prepaid;
|
||||
|
||||
return {
|
||||
amount: price ?? undefined,
|
||||
tiers: tiers,
|
||||
|
||||
interval: itemToBillingInterval({ item }),
|
||||
interval_count:
|
||||
item.interval_count !== 1 && typeof item.interval_count === "number"
|
||||
? item.interval_count
|
||||
: undefined,
|
||||
|
||||
billing_units: item.billing_units ?? 1,
|
||||
billing_method: billingMethod,
|
||||
max_purchase: maxPurchase,
|
||||
};
|
||||
};
|
||||
|
||||
const itemToPlanFeatureRollover = ({
|
||||
item,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
}): ApiPlanItemV1["rollover"] => {
|
||||
if (!item.config?.rollover) return undefined;
|
||||
|
||||
return {
|
||||
max: item.config.rollover.max ?? null,
|
||||
expiry_duration_type: item.config.rollover.duration,
|
||||
expiry_duration_length: item.config.rollover.length,
|
||||
};
|
||||
};
|
||||
|
||||
const itemToPlanFeatureProration = ({
|
||||
item,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
}): ApiPlanItemV1["proration"] => {
|
||||
if (!item.config?.on_increase || !item.config?.on_decrease) return undefined;
|
||||
|
||||
if (!isFeaturePriceItem(item)) return undefined;
|
||||
|
||||
return {
|
||||
on_increase: item.config.on_increase,
|
||||
on_decrease: item.config.on_decrease,
|
||||
};
|
||||
};
|
||||
|
||||
export const productItemsToPlanItemsV1 = ({
|
||||
items,
|
||||
features,
|
||||
expand = [],
|
||||
}: {
|
||||
items: ProductItem[];
|
||||
features: Feature[];
|
||||
expand?: string[];
|
||||
}): ApiPlanItemV1[] => {
|
||||
if (!items) return [];
|
||||
|
||||
const shouldExpandFeature = expandIncludes({
|
||||
expand,
|
||||
includes: [CusExpand.PlanFeaturesFeature],
|
||||
});
|
||||
|
||||
return items.map((item) => {
|
||||
const feature = features.find((f) => f.id === item.feature_id);
|
||||
if (!item.feature_id || !feature) {
|
||||
throw new InternalError({
|
||||
message:
|
||||
"Converting item to plan feature: item has no feature ID or feature not found",
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Included balance (V1 uses "included", not "granted_balance")
|
||||
const included =
|
||||
item.included_usage === Infinite ? 0 : (item.included_usage ?? 0);
|
||||
|
||||
const reset = itemToReset({ item, feature });
|
||||
const price = itemToPlanFeaturePrice({ item });
|
||||
const rollover = itemToPlanFeatureRollover({ item });
|
||||
const proration = itemToPlanFeatureProration({ item });
|
||||
|
||||
// Convert feature to API format if expand requested
|
||||
const apiFeature = shouldExpandFeature
|
||||
? toApiFeature({ feature })
|
||||
: undefined;
|
||||
|
||||
return ApiPlanItemV1Schema.parse({
|
||||
feature_id: item.feature_id,
|
||||
feature: apiFeature,
|
||||
included: included,
|
||||
unlimited: item.included_usage === Infinite,
|
||||
|
||||
reset,
|
||||
price, // V1: price can be null (no need for conditional spread)
|
||||
|
||||
rollover,
|
||||
proration,
|
||||
|
||||
display: getProductItemDisplay({ item, features }),
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user