feat: new attach / update subscription params
This commit is contained in:
38
server/src/external/autumn/autumnCli.ts
vendored
38
server/src/external/autumn/autumnCli.ts
vendored
@@ -11,8 +11,6 @@ import {
|
|||||||
type AttachBodyV0,
|
type AttachBodyV0,
|
||||||
type AttachParamsV0Input,
|
type AttachParamsV0Input,
|
||||||
type BalancesUpdateParams,
|
type BalancesUpdateParams,
|
||||||
type BillingPreviewResponse,
|
|
||||||
type BillingResponse,
|
|
||||||
type CheckQuery,
|
type CheckQuery,
|
||||||
type CreateBalanceParamsV0,
|
type CreateBalanceParamsV0,
|
||||||
type CreateCustomerInternalOptions,
|
type CreateCustomerInternalOptions,
|
||||||
@@ -263,13 +261,13 @@ export class AutumnInt {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async attach(
|
async attach<TInput = AttachBodyV0>(
|
||||||
params: AttachBodyV0,
|
params: TInput,
|
||||||
{
|
{
|
||||||
skipWebhooks,
|
skipWebhooks,
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
}: { skipWebhooks?: boolean; idempotencyKey?: string } = {},
|
}: { skipWebhooks?: boolean; idempotencyKey?: string } = {},
|
||||||
) {
|
): Promise<any> {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (skipWebhooks !== undefined) {
|
if (skipWebhooks !== undefined) {
|
||||||
headers["x-skip-webhooks"] = skipWebhooks ? "true" : "false";
|
headers["x-skip-webhooks"] = skipWebhooks ? "true" : "false";
|
||||||
@@ -815,13 +813,13 @@ export class AutumnInt {
|
|||||||
};
|
};
|
||||||
|
|
||||||
subscriptions = {
|
subscriptions = {
|
||||||
update: async (
|
update: async <TInput = UpdateSubscriptionV0Params>(
|
||||||
params: UpdateSubscriptionV0Params,
|
params: TInput,
|
||||||
{
|
{
|
||||||
timeout,
|
timeout,
|
||||||
skipWebhooks,
|
skipWebhooks,
|
||||||
}: { timeout?: number; skipWebhooks?: boolean } = {},
|
}: { timeout?: number; skipWebhooks?: boolean } = {},
|
||||||
): Promise<BillingResponse> => {
|
): Promise<any> => {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (skipWebhooks !== undefined) {
|
if (skipWebhooks !== undefined) {
|
||||||
headers["x-skip-webhooks"] = skipWebhooks ? "true" : "false";
|
headers["x-skip-webhooks"] = skipWebhooks ? "true" : "false";
|
||||||
@@ -838,15 +836,21 @@ export class AutumnInt {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
previewUpdate: async (params: UpdateSubscriptionV0Params) => {
|
previewUpdate: async <TInput = UpdateSubscriptionV0Params>(
|
||||||
|
params: TInput,
|
||||||
|
): Promise<any> => {
|
||||||
const data = await this.post(`/subscriptions/preview_update`, params);
|
const data = await this.post(`/subscriptions/preview_update`, params);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
billing = {
|
billing = {
|
||||||
attach: async (
|
attach: async <
|
||||||
params: Omit<AttachParamsV0Input, "items"> & { items?: ProductItem[] },
|
TInput = Omit<AttachParamsV0Input, "items"> & {
|
||||||
|
items?: ProductItem[];
|
||||||
|
},
|
||||||
|
>(
|
||||||
|
params: TInput,
|
||||||
{
|
{
|
||||||
skipWebhooks,
|
skipWebhooks,
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
@@ -856,7 +860,7 @@ export class AutumnInt {
|
|||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
} = {},
|
} = {},
|
||||||
) => {
|
): Promise<any> => {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (skipWebhooks !== undefined) {
|
if (skipWebhooks !== undefined) {
|
||||||
headers["x-skip-webhooks"] = skipWebhooks ? "true" : "false";
|
headers["x-skip-webhooks"] = skipWebhooks ? "true" : "false";
|
||||||
@@ -877,9 +881,13 @@ export class AutumnInt {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
previewAttach: async (
|
previewAttach: async <
|
||||||
params: Omit<AttachParamsV0Input, "items"> & { items?: ProductItem[] },
|
TInput = Omit<AttachParamsV0Input, "items"> & {
|
||||||
): Promise<BillingPreviewResponse> => {
|
items?: ProductItem[];
|
||||||
|
},
|
||||||
|
>(
|
||||||
|
params: TInput,
|
||||||
|
): Promise<any> => {
|
||||||
const data = await this.post(`/billing/preview_attach`, {
|
const data = await this.post(`/billing/preview_attach`, {
|
||||||
...params,
|
...params,
|
||||||
redirect_mode: "if_required",
|
redirect_mode: "if_required",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export const versionedValidator = ({
|
|||||||
fromVersion: userVersion,
|
fromVersion: userVersion,
|
||||||
toVersion: new ApiVersionClass(LATEST_VERSION),
|
toVersion: new ApiVersionClass(LATEST_VERSION),
|
||||||
resource,
|
resource,
|
||||||
|
ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace validated data with transformed version
|
// Replace validated data with transformed version
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import type {
|
||||||
type AttachBillingContext,
|
AttachBillingContext,
|
||||||
type AttachParamsV0,
|
AttachParamsV1,
|
||||||
type BillingContextOverride,
|
BillingContextOverride,
|
||||||
type BillingPlan,
|
BillingPlan,
|
||||||
type BillingResult,
|
BillingResult,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { computeAttachPlan } from "@/internal/billing/v2/actions/attach/compute/computeAttachPlan";
|
import { computeAttachPlan } from "@/internal/billing/v2/actions/attach/compute/computeAttachPlan";
|
||||||
@@ -31,7 +31,7 @@ export async function attach({
|
|||||||
contextOverride,
|
contextOverride,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
skipAutumnCheckout?: boolean;
|
skipAutumnCheckout?: boolean;
|
||||||
contextOverride?: BillingContextOverride;
|
contextOverride?: BillingContextOverride;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AttachBillingContext,
|
AttachBillingContext,
|
||||||
AttachParamsV0,
|
AttachParamsV1,
|
||||||
AutumnBillingPlan,
|
AutumnBillingPlan,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
@@ -25,7 +25,7 @@ export const computeAttachPlan = ({
|
|||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
attachBillingContext: AttachBillingContext;
|
attachBillingContext: AttachBillingContext;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
}): AutumnBillingPlan => {
|
}): AutumnBillingPlan => {
|
||||||
const {
|
const {
|
||||||
currentCustomerProduct,
|
currentCustomerProduct,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AttachBillingContext,
|
AttachBillingContext,
|
||||||
AttachParamsV0,
|
AttachParamsV1,
|
||||||
AutumnBillingPlan,
|
AutumnBillingPlan,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
@@ -19,7 +19,7 @@ export const finalizeAttachPlan = ({
|
|||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
plan: AutumnBillingPlan;
|
plan: AutumnBillingPlan;
|
||||||
attachBillingContext: AttachBillingContext;
|
attachBillingContext: AttachBillingContext;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
}): AutumnBillingPlan => {
|
}): AutumnBillingPlan => {
|
||||||
plan.lineItems = finalizeLineItems({
|
plan.lineItems = finalizeLineItems({
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AttachBillingContext,
|
AttachBillingContext,
|
||||||
AttachParamsV0,
|
AttachParamsV1,
|
||||||
BillingPlan,
|
BillingPlan,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
@@ -20,11 +20,11 @@ export async function createAutumnCheckout({
|
|||||||
billingPlan,
|
billingPlan,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
billingContext: AttachBillingContext;
|
billingContext: AttachBillingContext;
|
||||||
billingPlan: BillingPlan;
|
billingPlan: BillingPlan;
|
||||||
}): Promise<AttachResult> {
|
}): Promise<AttachResult> {
|
||||||
const { checkout, checkoutUrl } = await billingPlanToAutumnCheckout({
|
const { checkout } = await billingPlanToAutumnCheckout({
|
||||||
ctx,
|
ctx,
|
||||||
params,
|
params,
|
||||||
billingContext,
|
billingContext,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
|
type AttachBillingContext,
|
||||||
|
type AttachParamsV1,
|
||||||
|
type AutumnBillingPlan,
|
||||||
cusProductToPrices,
|
cusProductToPrices,
|
||||||
ErrCode,
|
ErrCode,
|
||||||
isFreeProduct,
|
isFreeProduct,
|
||||||
RecaseError,
|
RecaseError,
|
||||||
type AttachBillingContext,
|
|
||||||
type AttachParamsV0,
|
|
||||||
type AutumnBillingPlan,
|
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition";
|
import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition";
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export const handleAttachBillingBehaviorErrors = ({
|
|||||||
}: {
|
}: {
|
||||||
billingContext: AttachBillingContext;
|
billingContext: AttachBillingContext;
|
||||||
autumnBillingPlan: AutumnBillingPlan;
|
autumnBillingPlan: AutumnBillingPlan;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
}) => {
|
}) => {
|
||||||
// Only validate when billing_behavior is 'next_cycle_only' (defer charges)
|
// Only validate when billing_behavior is 'next_cycle_only' (defer charges)
|
||||||
if (params.billing_behavior !== "next_cycle_only") return;
|
if (params.billing_behavior !== "next_cycle_only") return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AttachBillingContext,
|
AttachBillingContext,
|
||||||
AttachParamsV0,
|
AttachParamsV1,
|
||||||
AutumnBillingPlan,
|
AutumnBillingPlan,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
@@ -24,7 +24,7 @@ export const handleAttachV2Errors = ({
|
|||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
billingContext: AttachBillingContext;
|
billingContext: AttachBillingContext;
|
||||||
autumnBillingPlan: AutumnBillingPlan;
|
autumnBillingPlan: AutumnBillingPlan;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
}) => {
|
}) => {
|
||||||
// 1. External PSP errors (RevenueCat)
|
// 1. External PSP errors (RevenueCat)
|
||||||
handleExternalPSPErrors({
|
handleExternalPSPErrors({
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
AttachBillingContext,
|
AttachBillingContext,
|
||||||
|
AttachParamsV1,
|
||||||
BillingContextOverride,
|
BillingContextOverride,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import {
|
import { BillingVersion, notNullish } from "@autumn/shared";
|
||||||
type AttachParamsV0,
|
|
||||||
BillingVersion,
|
|
||||||
notNullish,
|
|
||||||
} from "@autumn/shared";
|
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { setupStripeBillingContext } from "@/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext";
|
import { setupStripeBillingContext } from "@/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext";
|
||||||
import { setupBillingCycleAnchor } from "@/internal/billing/v2/setup/setupBillingCycleAnchor";
|
import { setupBillingCycleAnchor } from "@/internal/billing/v2/setup/setupBillingCycleAnchor";
|
||||||
@@ -30,7 +27,7 @@ export const setupAttachBillingContext = async ({
|
|||||||
contextOverride = {},
|
contextOverride = {},
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
contextOverride?: BillingContextOverride;
|
contextOverride?: BillingContextOverride;
|
||||||
}): Promise<AttachBillingContext> => {
|
}): Promise<AttachBillingContext> => {
|
||||||
const { fullCustomer: fullCustomerOverride } = contextOverride;
|
const { fullCustomer: fullCustomerOverride } = contextOverride;
|
||||||
@@ -84,7 +81,7 @@ export const setupAttachBillingContext = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const invoiceMode = setupInvoiceModeContext({ params });
|
const invoiceMode = setupInvoiceModeContext({ params });
|
||||||
const isCustom = notNullish(params.items);
|
const isCustom = notNullish(params.customize);
|
||||||
|
|
||||||
// Timestamp context
|
// Timestamp context
|
||||||
const currentEpochMs = testClockFrozenTime ?? Date.now();
|
const currentEpochMs = testClockFrozenTime ?? Date.now();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AttachParamsV0, BillingContextOverride } from "@autumn/shared";
|
import type { AttachParamsV1, BillingContextOverride } from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { ProductService } from "@/internal/products/ProductService";
|
import { ProductService } from "@/internal/products/ProductService";
|
||||||
import { setupCustomFullProduct } from "../../../setup/setupCustomFullProduct";
|
import { setupCustomFullProduct } from "../../../setup/setupCustomFullProduct";
|
||||||
@@ -12,7 +12,7 @@ export const setupAttachProductContext = async ({
|
|||||||
contextOverride = {},
|
contextOverride = {},
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
contextOverride?: BillingContextOverride;
|
contextOverride?: BillingContextOverride;
|
||||||
}) => {
|
}) => {
|
||||||
const { productContext } = contextOverride;
|
const { productContext } = contextOverride;
|
||||||
@@ -37,7 +37,7 @@ export const setupAttachProductContext = async ({
|
|||||||
} = await setupCustomFullProduct({
|
} = await setupCustomFullProduct({
|
||||||
ctx,
|
ctx,
|
||||||
currentFullProduct: fullProduct,
|
currentFullProduct: fullProduct,
|
||||||
customItems: params.items,
|
customizePlan: params.customize,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
type AttachParamsV0,
|
type AttachParamsV1,
|
||||||
type FullCusProduct,
|
type FullCusProduct,
|
||||||
type FullCustomer,
|
type FullCustomer,
|
||||||
type FullProduct,
|
type FullProduct,
|
||||||
@@ -30,7 +30,7 @@ export const setupAttachTrialContext = async ({
|
|||||||
currentContext,
|
currentContext,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
currentContext: {
|
currentContext: {
|
||||||
fullCustomer: FullCustomer;
|
fullCustomer: FullCustomer;
|
||||||
attachProduct: FullProduct;
|
attachProduct: FullProduct;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { BillingContextOverride, PlanTiming } from "@autumn/shared";
|
import type {
|
||||||
import { type AttachParamsV0, BillingVersion } from "@autumn/shared";
|
AttachParamsV1,
|
||||||
|
BillingContextOverride,
|
||||||
|
PlanTiming,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { BillingVersion } from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { billingActions } from "@/internal/billing/v2/actions";
|
import { billingActions } from "@/internal/billing/v2/actions";
|
||||||
import { attachParamsToStripeBillingContext } from "@/internal/billing/v2/actions/legacy/utils/attachParamsToStripeBillingContext";
|
import { attachParamsToStripeBillingContext } from "@/internal/billing/v2/actions/legacy/utils/attachParamsToStripeBillingContext";
|
||||||
@@ -44,14 +48,10 @@ export const legacyAttach = async ({
|
|||||||
|
|
||||||
const fullCustomer = attachParams.customer;
|
const fullCustomer = attachParams.customer;
|
||||||
|
|
||||||
const params: AttachParamsV0 = {
|
const params: AttachParamsV1 = {
|
||||||
customer_id: fullCustomer.id || fullCustomer.internal_id,
|
customer_id: fullCustomer.id || fullCustomer.internal_id,
|
||||||
entity_id: fullCustomer.entity?.id,
|
entity_id: fullCustomer.entity?.id,
|
||||||
product_id: fullProduct.id,
|
product_id: fullProduct.id,
|
||||||
// items: body.items,
|
|
||||||
// version: body.version,
|
|
||||||
// invoice: body.invoice,
|
|
||||||
// free_trial: body.free_trial === false ? null : undefined,
|
|
||||||
|
|
||||||
invoice: attachParams.invoiceOnly,
|
invoice: attachParams.invoiceOnly,
|
||||||
enable_product_immediately: true,
|
enable_product_immediately: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
UpdateSubscriptionBillingContextOverride,
|
UpdateSubscriptionBillingContextOverride,
|
||||||
UpdateSubscriptionV0Params,
|
UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import {
|
import {
|
||||||
type AttachBodyV0,
|
type AttachBodyV0,
|
||||||
@@ -63,7 +63,7 @@ export const renew = async ({
|
|||||||
|
|
||||||
const fullCustomer = attachParams.customer;
|
const fullCustomer = attachParams.customer;
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: fullCustomer.id || fullCustomer.internal_id,
|
customer_id: fullCustomer.id || fullCustomer.internal_id,
|
||||||
entity_id: fullCustomer.entity?.id,
|
entity_id: fullCustomer.entity?.id,
|
||||||
product_id: fullProduct.id,
|
product_id: fullProduct.id,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
UpdateSubscriptionBillingContextOverride,
|
UpdateSubscriptionBillingContextOverride,
|
||||||
UpdateSubscriptionV0Params,
|
UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import {
|
import {
|
||||||
type AttachBodyV0,
|
type AttachBodyV0,
|
||||||
@@ -64,7 +64,7 @@ export const updateQuantity = async ({
|
|||||||
|
|
||||||
const fullCustomer = attachParams.customer;
|
const fullCustomer = attachParams.customer;
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: fullCustomer.id || fullCustomer.internal_id,
|
customer_id: fullCustomer.id || fullCustomer.internal_id,
|
||||||
entity_id: fullCustomer.entity?.id,
|
entity_id: fullCustomer.entity?.id,
|
||||||
product_id: fullProduct.id,
|
product_id: fullProduct.id,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
type FullCustomer,
|
type FullCustomer,
|
||||||
type FullProduct,
|
type FullProduct,
|
||||||
featureUtils,
|
featureUtils,
|
||||||
type UpdateSubscriptionV0Params,
|
type UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { FeatureOptionsParamsV0 } from "@shared/api/billing/common/featureOptions/featureOptionsParamsV0";
|
import type { FeatureOptionsParamsV0 } from "@shared/api/billing/common/featureOptions/featureOptionsParamsV0";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
@@ -45,7 +45,7 @@ export async function migrate({
|
|||||||
reset_after_trial_end: true,
|
reset_after_trial_end: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const updateSubscriptionParams: UpdateSubscriptionV0Params = {
|
const updateSubscriptionParams: UpdateSubscriptionV1Params = {
|
||||||
customer_id: fullCustomer.id || fullCustomer.internal_id,
|
customer_id: fullCustomer.id || fullCustomer.internal_id,
|
||||||
customer_product_id: currentCustomerProduct.id,
|
customer_product_id: currentCustomerProduct.id,
|
||||||
entity_id: entity?.id,
|
entity_id: entity?.id,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { UpdateSubscriptionV0Params } from "@shared/index";
|
import type { UpdateSubscriptionV1Params } from "@shared/index";
|
||||||
|
|
||||||
export enum UpdateSubscriptionIntent {
|
export enum UpdateSubscriptionIntent {
|
||||||
UpdateQuantity = "update_quantity",
|
UpdateQuantity = "update_quantity",
|
||||||
@@ -10,9 +10,9 @@ export enum UpdateSubscriptionIntent {
|
|||||||
* Compute the intent for a subscription update
|
* Compute the intent for a subscription update
|
||||||
*/
|
*/
|
||||||
export const computeUpdateSubscriptionIntent = (
|
export const computeUpdateSubscriptionIntent = (
|
||||||
params: UpdateSubscriptionV0Params,
|
params: UpdateSubscriptionV1Params,
|
||||||
): UpdateSubscriptionIntent => {
|
): UpdateSubscriptionIntent => {
|
||||||
const itemsChanged = params.items !== undefined;
|
const itemsChanged = params.customize !== undefined;
|
||||||
const versionChanged = params.version !== undefined;
|
const versionChanged = params.version !== undefined;
|
||||||
const freeTrialChanged = params.free_trial !== undefined;
|
const freeTrialChanged = params.free_trial !== undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { UpdateSubscriptionV0Params } from "@autumn/shared";
|
import type {
|
||||||
|
AutumnBillingPlan,
|
||||||
|
UpdateSubscriptionBillingContext,
|
||||||
|
UpdateSubscriptionV1Params,
|
||||||
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import type { UpdateSubscriptionBillingContext } from "@autumn/shared";
|
|
||||||
import type { AutumnBillingPlan } from "@autumn/shared";
|
|
||||||
|
|
||||||
import { computeCancelPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan";
|
import { computeCancelPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan";
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ export const computeUpdateSubscriptionPlan = async ({
|
|||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
billingContext: UpdateSubscriptionBillingContext;
|
billingContext: UpdateSubscriptionBillingContext;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
}): Promise<AutumnBillingPlan> => {
|
}): Promise<AutumnBillingPlan> => {
|
||||||
const intent = computeUpdateSubscriptionIntent(params);
|
const intent = computeUpdateSubscriptionIntent(params);
|
||||||
|
|
||||||
@@ -39,7 +41,6 @@ export const computeUpdateSubscriptionPlan = async ({
|
|||||||
plan = await computeCustomPlan({
|
plan = await computeCustomPlan({
|
||||||
ctx,
|
ctx,
|
||||||
updateSubscriptionContext: billingContext,
|
updateSubscriptionContext: billingContext,
|
||||||
params,
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case UpdateSubscriptionIntent.None:
|
case UpdateSubscriptionIntent.None:
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import type {
|
|||||||
AutumnBillingPlan,
|
AutumnBillingPlan,
|
||||||
UpdateSubscriptionBillingContext,
|
UpdateSubscriptionBillingContext,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import {
|
import { CusProductStatus } from "@autumn/shared";
|
||||||
CusProductStatus,
|
|
||||||
type UpdateSubscriptionV0Params,
|
|
||||||
} from "@autumn/shared";
|
|
||||||
import type { AutumnContext } from "@server/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@server/honoUtils/HonoEnv";
|
||||||
import { computeDeleteCustomerProduct } from "@/internal/billing/v2/actions/updateSubscription/compute/computeDeleteCustomerProduct";
|
import { computeDeleteCustomerProduct } from "@/internal/billing/v2/actions/updateSubscription/compute/computeDeleteCustomerProduct";
|
||||||
import { computeCustomPlanNewCustomerProduct } from "@/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct";
|
import { computeCustomPlanNewCustomerProduct } from "@/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct";
|
||||||
@@ -14,11 +11,9 @@ import { buildAutumnLineItems } from "@/internal/billing/v2/compute/computeAutum
|
|||||||
export const computeCustomPlan = async ({
|
export const computeCustomPlan = async ({
|
||||||
ctx,
|
ctx,
|
||||||
updateSubscriptionContext,
|
updateSubscriptionContext,
|
||||||
params,
|
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
updateSubscriptionContext: UpdateSubscriptionBillingContext;
|
updateSubscriptionContext: UpdateSubscriptionBillingContext;
|
||||||
params: UpdateSubscriptionV0Params;
|
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
customerProduct,
|
customerProduct,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
type AutumnBillingPlan,
|
type AutumnBillingPlan,
|
||||||
isCustomerProductOneOff,
|
isCustomerProductOneOff,
|
||||||
type UpdateSubscriptionBillingContext,
|
type UpdateSubscriptionBillingContext,
|
||||||
type UpdateSubscriptionV0Params,
|
type UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { finalizeLineItems } from "@/internal/billing/v2/compute/finalize/finalizeLineItems";
|
import { finalizeLineItems } from "@/internal/billing/v2/compute/finalize/finalizeLineItems";
|
||||||
@@ -20,7 +20,7 @@ export const finalizeUpdateSubscriptionPlan = ({
|
|||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
plan: AutumnBillingPlan;
|
plan: AutumnBillingPlan;
|
||||||
billingContext: UpdateSubscriptionBillingContext;
|
billingContext: UpdateSubscriptionBillingContext;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
}): AutumnBillingPlan => {
|
}): AutumnBillingPlan => {
|
||||||
// Finalize line items (shared logic)
|
// Finalize line items (shared logic)
|
||||||
plan.lineItems = finalizeLineItems({
|
plan.lineItems = finalizeLineItems({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
ErrCode,
|
ErrCode,
|
||||||
isFreeProduct,
|
isFreeProduct,
|
||||||
RecaseError,
|
RecaseError,
|
||||||
type UpdateSubscriptionV0Params,
|
type UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition";
|
import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition";
|
||||||
import {
|
import {
|
||||||
@@ -22,7 +22,7 @@ export const handleBillingBehaviorErrors = ({
|
|||||||
}: {
|
}: {
|
||||||
billingContext: UpdateSubscriptionBillingContext;
|
billingContext: UpdateSubscriptionBillingContext;
|
||||||
billingPlan: BillingPlan;
|
billingPlan: BillingPlan;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
}) => {
|
}) => {
|
||||||
const { autumn: autumnBillingPlan } = billingPlan;
|
const { autumn: autumnBillingPlan } = billingPlan;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
import type {
|
||||||
|
AutumnBillingPlan,
|
||||||
|
UpdateSubscriptionBillingContext,
|
||||||
|
} from "@autumn/shared";
|
||||||
import {
|
import {
|
||||||
cusProductToProduct,
|
cusProductToProduct,
|
||||||
productsAreSame,
|
productsAreSame,
|
||||||
RecaseError,
|
RecaseError,
|
||||||
type UpdateSubscriptionV0Params,
|
type UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import type { UpdateSubscriptionBillingContext } from "@autumn/shared";
|
|
||||||
import type { AutumnBillingPlan } from "@autumn/shared";
|
|
||||||
|
|
||||||
export const handleCustomPlanErrors = ({
|
export const handleCustomPlanErrors = ({
|
||||||
ctx,
|
ctx,
|
||||||
@@ -17,9 +19,9 @@ export const handleCustomPlanErrors = ({
|
|||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
billingContext: UpdateSubscriptionBillingContext;
|
billingContext: UpdateSubscriptionBillingContext;
|
||||||
autumnBillingPlan: AutumnBillingPlan;
|
autumnBillingPlan: AutumnBillingPlan;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
}) => {
|
}) => {
|
||||||
if (!params.items) return;
|
if (!params.customize) return;
|
||||||
|
|
||||||
const newCustomerProduct = autumnBillingPlan.insertCustomerProducts?.[0];
|
const newCustomerProduct = autumnBillingPlan.insertCustomerProducts?.[0];
|
||||||
const currentCustomerProduct = billingContext.customerProduct;
|
const currentCustomerProduct = billingContext.customerProduct;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
AutumnBillingPlan,
|
AutumnBillingPlan,
|
||||||
UpdateSubscriptionBillingContext,
|
UpdateSubscriptionBillingContext,
|
||||||
|
UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import {
|
import {
|
||||||
cusProductToPrices,
|
cusProductToPrices,
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
nullish,
|
nullish,
|
||||||
priceToFeature,
|
priceToFeature,
|
||||||
RecaseError,
|
RecaseError,
|
||||||
type UpdateSubscriptionV0Params,
|
|
||||||
type UsagePriceConfig,
|
type UsagePriceConfig,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
@@ -22,7 +22,7 @@ const checkInputFeatureQuantitiesAreValid = ({
|
|||||||
billingContext,
|
billingContext,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
autumnBillingPlan: AutumnBillingPlan;
|
autumnBillingPlan: AutumnBillingPlan;
|
||||||
billingContext: UpdateSubscriptionBillingContext;
|
billingContext: UpdateSubscriptionBillingContext;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -66,7 +66,7 @@ export const handleFeatureQuantityErrors = ({
|
|||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
billingContext: UpdateSubscriptionBillingContext;
|
billingContext: UpdateSubscriptionBillingContext;
|
||||||
autumnBillingPlan: AutumnBillingPlan;
|
autumnBillingPlan: AutumnBillingPlan;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
}) => {
|
}) => {
|
||||||
// 1. Check if param feature IDs are valid
|
// 1. Check if param feature IDs are valid
|
||||||
checkInputFeatureQuantitiesAreValid({
|
checkInputFeatureQuantitiesAreValid({
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import type {
|
||||||
|
AutumnBillingPlan,
|
||||||
|
UpdateSubscriptionBillingContext,
|
||||||
|
} from "@autumn/shared";
|
||||||
import {
|
import {
|
||||||
cusProductToProduct,
|
cusProductToProduct,
|
||||||
isCustomerProductOneOff,
|
isCustomerProductOneOff,
|
||||||
@@ -5,12 +9,10 @@ import {
|
|||||||
isOneOffPrice,
|
isOneOffPrice,
|
||||||
productsAreSame,
|
productsAreSame,
|
||||||
RecaseError,
|
RecaseError,
|
||||||
type UpdateSubscriptionV0Params,
|
type UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { cusProductToPrices } from "@shared/utils/cusProductUtils/convertCusProduct";
|
import { cusProductToPrices } from "@shared/utils/cusProductUtils/convertCusProduct";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import type { UpdateSubscriptionBillingContext } from "@autumn/shared";
|
|
||||||
import type { AutumnBillingPlan } from "@autumn/shared";
|
|
||||||
import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition";
|
import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition";
|
||||||
|
|
||||||
export const handleOneOffErrors = ({
|
export const handleOneOffErrors = ({
|
||||||
@@ -22,7 +24,7 @@ export const handleOneOffErrors = ({
|
|||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
billingContext: UpdateSubscriptionBillingContext;
|
billingContext: UpdateSubscriptionBillingContext;
|
||||||
autumnBillingPlan: AutumnBillingPlan;
|
autumnBillingPlan: AutumnBillingPlan;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
}) => {
|
}) => {
|
||||||
const { customerProduct } = billingContext;
|
const { customerProduct } = billingContext;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
BillingPlan,
|
BillingPlan,
|
||||||
UpdateSubscriptionBillingContext,
|
UpdateSubscriptionBillingContext,
|
||||||
UpdateSubscriptionV0Params,
|
UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { handleCancelEndOfCycleErrors } from "@/internal/billing/v2/actions/updateSubscription/errors/handleCancelEndOfCycleErrors";
|
import { handleCancelEndOfCycleErrors } from "@/internal/billing/v2/actions/updateSubscription/errors/handleCancelEndOfCycleErrors";
|
||||||
@@ -28,7 +28,7 @@ export const handleUpdateSubscriptionErrors = async ({
|
|||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
billingContext: UpdateSubscriptionBillingContext;
|
billingContext: UpdateSubscriptionBillingContext;
|
||||||
billingPlan: BillingPlan;
|
billingPlan: BillingPlan;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
}) => {
|
}) => {
|
||||||
const { customerProduct } = billingContext;
|
const { customerProduct } = billingContext;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
type FullCustomer,
|
type FullCustomer,
|
||||||
isCusProductOnEntity,
|
isCusProductOnEntity,
|
||||||
type UpdateSubscriptionV0Params,
|
type UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
|
|
||||||
export const findTargetCustomerProduct = ({
|
export const findTargetCustomerProduct = ({
|
||||||
params,
|
params,
|
||||||
fullCustomer,
|
fullCustomer,
|
||||||
}: {
|
}: {
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
fullCustomer: FullCustomer;
|
fullCustomer: FullCustomer;
|
||||||
}) => {
|
}) => {
|
||||||
const cusProducts = fullCustomer.customer_products;
|
const cusProducts = fullCustomer.customer_products;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
type FullCusProduct,
|
type FullCusProduct,
|
||||||
type FullProduct,
|
type FullProduct,
|
||||||
nullish,
|
nullish,
|
||||||
type UpdateSubscriptionV0Params,
|
type UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { getFreeDefaultProductByGroup } from "@/internal/customers/cusProducts/cusProductUtils";
|
import { getFreeDefaultProductByGroup } from "@/internal/customers/cusProducts/cusProductUtils";
|
||||||
@@ -18,7 +18,7 @@ export const setupDefaultProductContext = async ({
|
|||||||
customerProduct,
|
customerProduct,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
customerProduct: FullCusProduct;
|
customerProduct: FullCusProduct;
|
||||||
}): Promise<FullProduct | undefined> => {
|
}): Promise<FullProduct | undefined> => {
|
||||||
// Only fetch if cancel is requested (not null/undefined)
|
// Only fetch if cancel is requested (not null/undefined)
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
UpdateSubscriptionBillingContext,
|
UpdateSubscriptionBillingContext,
|
||||||
UpdateSubscriptionBillingContextOverride,
|
UpdateSubscriptionBillingContextOverride,
|
||||||
|
UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import {
|
import { BillingVersion, notNullish } from "@autumn/shared";
|
||||||
BillingVersion,
|
|
||||||
notNullish,
|
|
||||||
type UpdateSubscriptionV0Params,
|
|
||||||
} from "@autumn/shared";
|
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { setupDefaultProductContext } from "@/internal/billing/v2/actions/updateSubscription/setup/setupDefaultProductContext";
|
import { setupDefaultProductContext } from "@/internal/billing/v2/actions/updateSubscription/setup/setupDefaultProductContext";
|
||||||
import { setupUpdateSubscriptionProductContext } from "@/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext";
|
import { setupUpdateSubscriptionProductContext } from "@/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext";
|
||||||
@@ -31,7 +28,7 @@ export const setupUpdateSubscriptionBillingContext = async ({
|
|||||||
contextOverride = {},
|
contextOverride = {},
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
contextOverride?: UpdateSubscriptionBillingContextOverride;
|
contextOverride?: UpdateSubscriptionBillingContextOverride;
|
||||||
}): Promise<UpdateSubscriptionBillingContext> => {
|
}): Promise<UpdateSubscriptionBillingContext> => {
|
||||||
const fullCustomer = await setupFullCustomerContext({
|
const fullCustomer = await setupFullCustomerContext({
|
||||||
@@ -101,7 +98,7 @@ export const setupUpdateSubscriptionBillingContext = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const invoiceMode = setupInvoiceModeContext({ params });
|
const invoiceMode = setupInvoiceModeContext({ params });
|
||||||
const isCustom = notNullish(params.items);
|
const isCustom = notNullish(params.customize);
|
||||||
|
|
||||||
const defaultProduct = await setupDefaultProductContext({
|
const defaultProduct = await setupDefaultProductContext({
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
notNullish,
|
notNullish,
|
||||||
RecaseError,
|
RecaseError,
|
||||||
type UpdateSubscriptionBillingContextOverride,
|
type UpdateSubscriptionBillingContextOverride,
|
||||||
type UpdateSubscriptionV0Params,
|
type UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { ProductService } from "@/internal/products/ProductService";
|
import { ProductService } from "@/internal/products/ProductService";
|
||||||
@@ -19,7 +19,7 @@ export const setupUpdateSubscriptionProductContext = async ({
|
|||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
fullCustomer: FullCustomer;
|
fullCustomer: FullCustomer;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
contextOverride?: UpdateSubscriptionBillingContextOverride;
|
contextOverride?: UpdateSubscriptionBillingContextOverride;
|
||||||
}) => {
|
}) => {
|
||||||
const { productContext } = contextOverride;
|
const { productContext } = contextOverride;
|
||||||
@@ -59,7 +59,7 @@ export const setupUpdateSubscriptionProductContext = async ({
|
|||||||
} = await setupCustomFullProduct({
|
} = await setupCustomFullProduct({
|
||||||
ctx,
|
ctx,
|
||||||
currentFullProduct: fullProduct,
|
currentFullProduct: fullProduct,
|
||||||
customItems: params.items,
|
customizePlan: params.customize,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
BillingParamsBase,
|
BillingParamsBaseV1,
|
||||||
FullCusProduct,
|
FullCusProduct,
|
||||||
FullProduct,
|
FullProduct,
|
||||||
TrialContext,
|
TrialContext,
|
||||||
@@ -33,7 +33,7 @@ export const setupUpdateSubscriptionTrialContext = ({
|
|||||||
customerProduct?: FullCusProduct;
|
customerProduct?: FullCusProduct;
|
||||||
currentEpochMs: number;
|
currentEpochMs: number;
|
||||||
fullProduct: FullProduct;
|
fullProduct: FullProduct;
|
||||||
params: BillingParamsBase;
|
params: BillingParamsBaseV1;
|
||||||
}): TrialContext | undefined => {
|
}): TrialContext | undefined => {
|
||||||
// Handle explicit free_trial param (null or value)
|
// Handle explicit free_trial param (null or value)
|
||||||
if (params.free_trial !== undefined) {
|
if (params.free_trial !== undefined) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {
|
|||||||
BillingResult,
|
BillingResult,
|
||||||
UpdateSubscriptionBillingContext,
|
UpdateSubscriptionBillingContext,
|
||||||
UpdateSubscriptionBillingContextOverride,
|
UpdateSubscriptionBillingContextOverride,
|
||||||
UpdateSubscriptionV0Params,
|
UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan";
|
import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan";
|
||||||
@@ -23,7 +23,7 @@ export async function updateSubscription({
|
|||||||
contextOverride,
|
contextOverride,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
contextOverride?: UpdateSubscriptionBillingContextOverride;
|
contextOverride?: UpdateSubscriptionBillingContextOverride;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
BillingParamsBase,
|
BillingParamsBaseV1,
|
||||||
EntitlementWithFeature,
|
EntitlementWithFeature,
|
||||||
FeatureOptions,
|
FeatureOptions,
|
||||||
Price,
|
Price,
|
||||||
@@ -12,7 +12,7 @@ export const paramsToFeatureOptions = ({
|
|||||||
price,
|
price,
|
||||||
entitlement,
|
entitlement,
|
||||||
}: {
|
}: {
|
||||||
params: BillingParamsBase;
|
params: BillingParamsBaseV1;
|
||||||
price: Price;
|
price: Price;
|
||||||
entitlement: EntitlementWithFeature;
|
entitlement: EntitlementWithFeature;
|
||||||
}): FeatureOptions | undefined => {
|
}): FeatureOptions | undefined => {
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { AttachParamsV0Schema, InternalError } from "@autumn/shared";
|
import {
|
||||||
|
AffectedResource,
|
||||||
|
ApiVersion,
|
||||||
|
AttachParamsV0Schema,
|
||||||
|
AttachParamsV1Schema,
|
||||||
|
InternalError,
|
||||||
|
} from "@autumn/shared";
|
||||||
import { billingActions } from "@/internal/billing/v2/actions";
|
import { billingActions } from "@/internal/billing/v2/actions";
|
||||||
import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
||||||
import { billingResultToResponse } from "../utils/billingResult/billingResultToResponse";
|
import { billingResultToResponse } from "../utils/billingResult/billingResultToResponse";
|
||||||
|
|
||||||
export const handleAttachV2 = createRoute({
|
export const handleAttachV2 = createRoute({
|
||||||
body: AttachParamsV0Schema,
|
versionedBody: {
|
||||||
|
latest: AttachParamsV1Schema,
|
||||||
|
[ApiVersion.V1_Beta]: AttachParamsV0Schema,
|
||||||
|
},
|
||||||
|
resource: AffectedResource.Attach,
|
||||||
lock:
|
lock:
|
||||||
process.env.NODE_ENV !== "development"
|
process.env.NODE_ENV !== "development"
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { AttachParamsV0Schema } from "@autumn/shared";
|
import {
|
||||||
|
AffectedResource,
|
||||||
|
ApiVersion,
|
||||||
|
AttachParamsV0Schema,
|
||||||
|
AttachParamsV1Schema,
|
||||||
|
} from "@autumn/shared";
|
||||||
import { billingActions } from "@/internal/billing/v2/actions";
|
import { billingActions } from "@/internal/billing/v2/actions";
|
||||||
import { billingPlanToChanges } from "@/internal/billing/v2/utils/billingPlanToChanges.js";
|
import { billingPlanToChanges } from "@/internal/billing/v2/utils/billingPlanToChanges.js";
|
||||||
import { billingPlanToPreviewResponse } from "@/internal/billing/v2/utils/billingPlanToPreviewResponse";
|
import { billingPlanToPreviewResponse } from "@/internal/billing/v2/utils/billingPlanToPreviewResponse";
|
||||||
import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
||||||
|
|
||||||
export const handlePreviewAttach = createRoute({
|
export const handlePreviewAttach = createRoute({
|
||||||
body: AttachParamsV0Schema,
|
versionedBody: {
|
||||||
|
latest: AttachParamsV1Schema,
|
||||||
|
[ApiVersion.V1_Beta]: AttachParamsV0Schema,
|
||||||
|
},
|
||||||
|
resource: AffectedResource.Attach,
|
||||||
lock:
|
lock:
|
||||||
process.env.NODE_ENV !== "development"
|
process.env.NODE_ENV !== "development"
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { UpdateSubscriptionV0ParamsSchema } from "@autumn/shared";
|
import {
|
||||||
|
AffectedResource,
|
||||||
|
ApiVersion,
|
||||||
|
UpdateSubscriptionV0ParamsSchema,
|
||||||
|
UpdateSubscriptionV1ParamsSchema,
|
||||||
|
} from "@autumn/shared";
|
||||||
import { billingActions } from "@/internal/billing/v2/actions";
|
import { billingActions } from "@/internal/billing/v2/actions";
|
||||||
import { billingPlanToPreviewResponse } from "@/internal/billing/v2/utils/billingPlanToPreviewResponse";
|
import { billingPlanToPreviewResponse } from "@/internal/billing/v2/utils/billingPlanToPreviewResponse";
|
||||||
import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
||||||
|
|
||||||
export const handlePreviewUpdateSubscription = createRoute({
|
export const handlePreviewUpdateSubscription = createRoute({
|
||||||
body: UpdateSubscriptionV0ParamsSchema,
|
versionedBody: {
|
||||||
|
latest: UpdateSubscriptionV1ParamsSchema,
|
||||||
|
[ApiVersion.V1_Beta]: UpdateSubscriptionV0ParamsSchema,
|
||||||
|
},
|
||||||
|
resource: AffectedResource.ApiSubscriptionUpdate,
|
||||||
handler: async (c) => {
|
handler: async (c) => {
|
||||||
const ctx = c.get("ctx");
|
const ctx = c.get("ctx");
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
|
AffectedResource,
|
||||||
|
ApiVersion,
|
||||||
InternalError,
|
InternalError,
|
||||||
UpdateSubscriptionV0ParamsSchema,
|
UpdateSubscriptionV0ParamsSchema,
|
||||||
|
UpdateSubscriptionV1ParamsSchema,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { billingActions } from "@/internal/billing/v2/actions";
|
import { billingActions } from "@/internal/billing/v2/actions";
|
||||||
import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
||||||
import { billingResultToResponse } from "../utils/billingResult/billingResultToResponse";
|
import { billingResultToResponse } from "../utils/billingResult/billingResultToResponse";
|
||||||
|
|
||||||
export const handleUpdateSubscription = createRoute({
|
export const handleUpdateSubscription = createRoute({
|
||||||
body: UpdateSubscriptionV0ParamsSchema,
|
versionedBody: {
|
||||||
|
latest: UpdateSubscriptionV1ParamsSchema,
|
||||||
|
[ApiVersion.V1_Beta]: UpdateSubscriptionV0ParamsSchema,
|
||||||
|
},
|
||||||
|
resource: AffectedResource.ApiSubscriptionUpdate,
|
||||||
lock:
|
lock:
|
||||||
process.env.NODE_ENV !== "development"
|
process.env.NODE_ENV !== "development"
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { UpdateSubscriptionV0Params } from "@shared/api/billing/updateSubscription/updateSubscriptionV0Params";
|
import type { CancelAction, UpdateSubscriptionV1Params } from "@autumn/shared";
|
||||||
import type { CancelAction } from "@autumn/shared";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup cancel action from params
|
* Setup cancel action from params
|
||||||
@@ -14,7 +13,7 @@ import type { CancelAction } from "@autumn/shared";
|
|||||||
export const setupCancelAction = ({
|
export const setupCancelAction = ({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: UpdateSubscriptionV0Params;
|
params: UpdateSubscriptionV1Params;
|
||||||
}): CancelAction | undefined => {
|
}): CancelAction | undefined => {
|
||||||
return params.cancel_action;
|
return params.cancel_action;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import type { FullProduct, ProductItem } from "@autumn/shared";
|
import {
|
||||||
|
type CustomizePlanV1,
|
||||||
|
customizePlanV1ToV0,
|
||||||
|
type FullProduct,
|
||||||
|
} from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
import { getEntsWithFeature } from "@/internal/products/entitlements/entitlementUtils";
|
import { getEntsWithFeature } from "@/internal/products/entitlements/entitlementUtils";
|
||||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems";
|
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems";
|
||||||
|
|
||||||
export const setupCustomFullProduct = async ({
|
export const setupCustomFullProduct = async ({
|
||||||
ctx,
|
ctx,
|
||||||
customItems,
|
// customItems,
|
||||||
currentFullProduct,
|
currentFullProduct,
|
||||||
|
customizePlan,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
customItems?: ProductItem[];
|
// customItems?: ProductItem[];
|
||||||
currentFullProduct: FullProduct;
|
currentFullProduct: FullProduct;
|
||||||
|
customizePlan?: CustomizePlanV1;
|
||||||
}) => {
|
}) => {
|
||||||
if (!customItems) {
|
if (!customizePlan) {
|
||||||
return {
|
return {
|
||||||
fullProduct: currentFullProduct,
|
fullProduct: currentFullProduct,
|
||||||
customPrices: [],
|
customPrices: [],
|
||||||
@@ -20,6 +26,8 @@ export const setupCustomFullProduct = async ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Customize plan -> custom items
|
||||||
|
|
||||||
const { db, logger, features } = ctx;
|
const { db, logger, features } = ctx;
|
||||||
|
|
||||||
const { prices: currentPrices, entitlements: currentEntitlements } =
|
const { prices: currentPrices, entitlements: currentEntitlements } =
|
||||||
@@ -30,7 +38,11 @@ export const setupCustomFullProduct = async ({
|
|||||||
db,
|
db,
|
||||||
curPrices: currentPrices,
|
curPrices: currentPrices,
|
||||||
curEnts: currentEntitlements,
|
curEnts: currentEntitlements,
|
||||||
newItems: customItems,
|
newItems: customizePlanV1ToV0({
|
||||||
|
ctx,
|
||||||
|
customizePlanV1: customizePlan,
|
||||||
|
fullProduct: currentFullProduct,
|
||||||
|
}),
|
||||||
features,
|
features,
|
||||||
product: currentFullProduct,
|
product: currentFullProduct,
|
||||||
logger,
|
logger,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
type BillingContextOverride,
|
type BillingContextOverride,
|
||||||
type BillingParamsBase,
|
type BillingParamsBaseV1,
|
||||||
cusProductToConvertedFeatureOptions,
|
cusProductToConvertedFeatureOptions,
|
||||||
type FeatureOptions,
|
type FeatureOptions,
|
||||||
type FullCusProduct,
|
type FullCusProduct,
|
||||||
@@ -25,7 +25,7 @@ export const setupFeatureQuantitiesContext = ({
|
|||||||
contextOverride = {},
|
contextOverride = {},
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
featureQuantitiesParams: BillingParamsBase;
|
featureQuantitiesParams: BillingParamsBaseV1;
|
||||||
fullProduct: FullProduct;
|
fullProduct: FullProduct;
|
||||||
currentCustomerProduct?: FullCusProduct;
|
currentCustomerProduct?: FullCusProduct;
|
||||||
initializeUndefinedQuantities?: boolean;
|
initializeUndefinedQuantities?: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { BillingParamsBase } from "@autumn/shared";
|
import type { BillingParamsBaseV1 } from "@autumn/shared";
|
||||||
import type { AutumnContext } from "@server/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@server/honoUtils/HonoEnv";
|
||||||
import { CusService } from "@server/internal/customers/CusService";
|
import { CusService } from "@server/internal/customers/CusService";
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ export const setupFullCustomerContext = async ({
|
|||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: BillingParamsBase;
|
params: BillingParamsBaseV1;
|
||||||
}) => {
|
}) => {
|
||||||
const { db, org, env } = ctx;
|
const { db, org, env } = ctx;
|
||||||
const { customer_id: customerId } = params;
|
const { customer_id: customerId } = params;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type {
|
import type {
|
||||||
AttachParamsV0,
|
AttachParamsV1,
|
||||||
UpdateSubscriptionV0Params,
|
UpdateSubscriptionV1Params,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
|
|
||||||
export const setupInvoiceModeContext = ({
|
export const setupInvoiceModeContext = ({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: UpdateSubscriptionV0Params | AttachParamsV0;
|
params: UpdateSubscriptionV1Params | AttachParamsV1;
|
||||||
}) => {
|
}) => {
|
||||||
if (params?.invoice !== true) {
|
if (params?.invoice !== true) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import type { BillingContextOverride, BillingParamsBase } from "@autumn/shared";
|
import type {
|
||||||
|
BillingContextOverride,
|
||||||
|
BillingParamsBaseV1,
|
||||||
|
} from "@autumn/shared";
|
||||||
import type { TransitionConfig } from "@models/billingModels/context/transitionConfig";
|
import type { TransitionConfig } from "@models/billingModels/context/transitionConfig";
|
||||||
|
|
||||||
export const setupTransitionConfigs = ({
|
export const setupTransitionConfigs = ({
|
||||||
params,
|
params,
|
||||||
contextOverride = {},
|
contextOverride = {},
|
||||||
}: {
|
}: {
|
||||||
params: BillingParamsBase;
|
params: BillingParamsBaseV1;
|
||||||
contextOverride?: BillingContextOverride;
|
contextOverride?: BillingContextOverride;
|
||||||
}): TransitionConfig[] => {
|
}): TransitionConfig[] => {
|
||||||
if (contextOverride.transitionConfigs) {
|
if (contextOverride.transitionConfigs) {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import {
|
|||||||
isCustomerProductTrialing,
|
isCustomerProductTrialing,
|
||||||
isProductPaidAndRecurring,
|
isProductPaidAndRecurring,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { FreeTrialParamsV0 } from "@shared/api/common/freeTrial/freeTrialParamsV0";
|
import type { FreeTrialParamsV1 } from "@shared/api/common/freeTrial/freeTrialParamsV1";
|
||||||
import type Stripe from "stripe";
|
import type Stripe from "stripe";
|
||||||
import { isStripeSubscriptionTrialing } from "@/external/stripe/subscriptions/utils/classifyStripeSubscriptionUtils";
|
import { isStripeSubscriptionTrialing } from "@/external/stripe/subscriptions/utils/classifyStripeSubscriptionUtils";
|
||||||
import { initFreeTrial } from "@/internal/products/free-trials/initFreeTrial";
|
import { initFreeTrialFromParamsV1 } from "@/internal/products/free-trials/initFreeTrialFromParamsV1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles explicit free_trial parameter passed to attach/update subscription.
|
* Handles explicit free_trial parameter passed to attach/update subscription.
|
||||||
@@ -22,7 +22,7 @@ export const handleFreeTrialParam = ({
|
|||||||
fullProduct,
|
fullProduct,
|
||||||
currentEpochMs,
|
currentEpochMs,
|
||||||
}: {
|
}: {
|
||||||
freeTrialParams: FreeTrialParamsV0 | null;
|
freeTrialParams: FreeTrialParamsV1 | null;
|
||||||
stripeSubscription?: Stripe.Subscription;
|
stripeSubscription?: Stripe.Subscription;
|
||||||
customerProduct?: FullCusProduct;
|
customerProduct?: FullCusProduct;
|
||||||
fullProduct: FullProduct;
|
fullProduct: FullProduct;
|
||||||
@@ -48,7 +48,7 @@ export const handleFreeTrialParam = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// free_trial: { length, duration } → Fresh trial
|
// free_trial: { length, duration } → Fresh trial
|
||||||
const dbFreeTrial = initFreeTrial({
|
const dbFreeTrial = initFreeTrialFromParamsV1({
|
||||||
freeTrialParams,
|
freeTrialParams,
|
||||||
internalProductId: fullProduct.internal_id,
|
internalProductId: fullProduct.internal_id,
|
||||||
isCustom: true,
|
isCustom: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
type AttachBillingContext,
|
type AttachBillingContext,
|
||||||
type AttachParamsV0,
|
type AttachParamsV1,
|
||||||
type BillingPlan,
|
type BillingPlan,
|
||||||
type Checkout,
|
type Checkout,
|
||||||
CheckoutAction,
|
CheckoutAction,
|
||||||
@@ -29,7 +29,7 @@ export async function billingPlanToAutumnCheckout({
|
|||||||
billingContext,
|
billingContext,
|
||||||
}: {
|
}: {
|
||||||
ctx: AutumnContext;
|
ctx: AutumnContext;
|
||||||
params: AttachParamsV0;
|
params: AttachParamsV1;
|
||||||
billingContext: AttachBillingContext;
|
billingContext: AttachBillingContext;
|
||||||
billingPlan: BillingPlan;
|
billingPlan: BillingPlan;
|
||||||
}): Promise<{ checkout: Checkout; checkoutUrl: string }> {
|
}): Promise<{ checkout: Checkout; checkoutUrl: string }> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
type AttachParamsV0,
|
type AttachParamsV1,
|
||||||
type Checkout,
|
type Checkout,
|
||||||
CheckoutAction,
|
CheckoutAction,
|
||||||
CheckoutStatus,
|
CheckoutStatus,
|
||||||
@@ -43,7 +43,7 @@ export const handleConfirmCheckout = createRoute({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = checkout.params as AttachParamsV0;
|
const params = checkout.params as AttachParamsV1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute attach (not preview mode)
|
// Execute attach (not preview mode)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
type AttachParamsV0,
|
type AttachParamsV1,
|
||||||
type Checkout,
|
type Checkout,
|
||||||
CheckoutAction,
|
CheckoutAction,
|
||||||
ErrCode,
|
ErrCode,
|
||||||
@@ -31,7 +31,7 @@ export const handleGetCheckout = createRoute({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = checkout.params as AttachParamsV0;
|
const params = checkout.params as AttachParamsV1;
|
||||||
|
|
||||||
// Re-run attach in preview mode to get current billing plan
|
// Re-run attach in preview mode to get current billing plan
|
||||||
const { billingContext, billingPlan } = await billingActions.attach({
|
const { billingContext, billingPlan } = await billingActions.attach({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
type AttachParamsV0,
|
type AttachParamsV1,
|
||||||
type Checkout,
|
type Checkout,
|
||||||
CheckoutAction,
|
CheckoutAction,
|
||||||
ErrCode,
|
ErrCode,
|
||||||
@@ -44,10 +44,10 @@ export const handlePreviewCheckout = createRoute({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalParams = checkout.params as AttachParamsV0;
|
const originalParams = checkout.params as AttachParamsV1;
|
||||||
|
|
||||||
// Merge provided options with original params
|
// Merge provided options with original params
|
||||||
const params: AttachParamsV0 = {
|
const params: AttachParamsV1 = {
|
||||||
...originalParams,
|
...originalParams,
|
||||||
options: body.options,
|
options: body.options,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { UpdateSubscriptionV0Params } from "@autumn/shared";
|
import type { UpdateSubscriptionV1Params } from "@autumn/shared";
|
||||||
import { createRoute } from "@/honoMiddlewares/routeHandler";
|
import { createRoute } from "@/honoMiddlewares/routeHandler";
|
||||||
import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan";
|
import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan";
|
||||||
import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors";
|
import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors";
|
||||||
@@ -23,7 +23,7 @@ export const handleCancelV2 = createRoute({
|
|||||||
prorate: bodyProrate = true,
|
prorate: bodyProrate = true,
|
||||||
} = await c.req.json();
|
} = await c.req.json();
|
||||||
|
|
||||||
const updateSubscriptionBody: UpdateSubscriptionV0Params = {
|
const updateSubscriptionBody: UpdateSubscriptionV1Params = {
|
||||||
customer_id,
|
customer_id,
|
||||||
product_id,
|
product_id,
|
||||||
entity_id,
|
entity_id,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { FreeTrial } from "@autumn/shared";
|
||||||
|
import type { FreeTrialParamsV1 } from "@shared/api/common/freeTrial/freeTrialParamsV1";
|
||||||
|
import { generateId } from "@/utils/genUtils";
|
||||||
|
|
||||||
|
export const initFreeTrialFromParamsV1 = ({
|
||||||
|
freeTrialParams,
|
||||||
|
internalProductId,
|
||||||
|
isCustom = false,
|
||||||
|
}: {
|
||||||
|
freeTrialParams: FreeTrialParamsV1;
|
||||||
|
internalProductId: string;
|
||||||
|
isCustom?: boolean;
|
||||||
|
}): FreeTrial => {
|
||||||
|
return {
|
||||||
|
id: generateId("ft"),
|
||||||
|
created_at: Date.now(),
|
||||||
|
internal_product_id: internalProductId,
|
||||||
|
is_custom: isCustom,
|
||||||
|
|
||||||
|
duration: freeTrialParams.duration_type,
|
||||||
|
length: freeTrialParams.duration_length,
|
||||||
|
card_required: freeTrialParams.card_required,
|
||||||
|
unique_fingerprint: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import type { ApiCustomerV3, AttachParamsV1Input } from "@autumn/shared";
|
||||||
|
import {
|
||||||
|
expectCustomerFeatureCorrect,
|
||||||
|
expectCustomerFeatureExists,
|
||||||
|
} from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
|
||||||
|
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
|
||||||
|
import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
|
||||||
|
import { TestFeature } from "@tests/setup/v2Features";
|
||||||
|
import { items } from "@tests/utils/fixtures/items";
|
||||||
|
import { itemsV2 } from "@tests/utils/fixtures/itemsV2";
|
||||||
|
import { products } from "@tests/utils/fixtures/products";
|
||||||
|
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { isPriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js";
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize attach: both customize.price + customize.items")}`, async () => {
|
||||||
|
const customerId = "v2-attach-customize-both";
|
||||||
|
|
||||||
|
const base = products.base({
|
||||||
|
id: "base",
|
||||||
|
items: [items.monthlyMessages({ includedUsage: 100 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [base] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: base.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
customize: {
|
||||||
|
price: itemsV2.monthlyPrice({ amount: 30 }),
|
||||||
|
items: [itemsV2.monthlyWords({ included: 250 }), itemsV2.dashboard()],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.billing.previewAttach<AttachParamsV1Input>(params);
|
||||||
|
expect(preview.total).toBe(30);
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductActive({ customer, productId: base.id });
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Words,
|
||||||
|
includedUsage: 250,
|
||||||
|
balance: 250,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
await expectCustomerFeatureExists({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Dashboard,
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 1,
|
||||||
|
latestTotal: 30,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize attach: only customize.price")}`, async () => {
|
||||||
|
const customerId = "v2-attach-customize-only-price";
|
||||||
|
|
||||||
|
const base = products.base({
|
||||||
|
id: "base",
|
||||||
|
items: [items.monthlyMessages({ includedUsage: 100 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [base] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: base.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
customize: {
|
||||||
|
price: itemsV2.monthlyPrice({ amount: 25 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.billing.previewAttach<AttachParamsV1Input>(params);
|
||||||
|
expect(preview.total).toBe(25);
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductActive({ customer, productId: base.id });
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
includedUsage: 100,
|
||||||
|
balance: 100,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 1,
|
||||||
|
latestTotal: 25,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize attach: only customize.items")}`, async () => {
|
||||||
|
const customerId = "v2-attach-customize-only-items";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
customize: {
|
||||||
|
// price: null,
|
||||||
|
items: [itemsV2.monthlyMessages({ included: 220 })],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.billing.previewAttach<AttachParamsV1Input>(params);
|
||||||
|
expect(preview.total).toBe(20);
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductActive({ customer, productId: pro.id });
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
includedUsage: 220,
|
||||||
|
balance: 220,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the original product price is still attached (price: null means keep original)
|
||||||
|
|
||||||
|
const customerProduct = customer.products.find((p) => p.id === pro.id);
|
||||||
|
const priceItem = customerProduct?.items?.find(isPriceItem);
|
||||||
|
expect(priceItem).toBeDefined();
|
||||||
|
expect(priceItem?.price).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize attach: price null makes product free")}`, async () => {
|
||||||
|
const customerId = "v2-attach-customize-price-null-free";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyWords({ includedUsage: 50 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
customize: {
|
||||||
|
price: null,
|
||||||
|
items: [itemsV2.monthlyMessages({ included: 200 })],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.billing.previewAttach<AttachParamsV1Input>(params);
|
||||||
|
expect(preview.total).toBe(0);
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductActive({ customer, productId: pro.id });
|
||||||
|
|
||||||
|
// Verify the customized feature is correct
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
includedUsage: 200,
|
||||||
|
balance: 200,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify no invoice was created (product is free)
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify no price item exists on the customer product
|
||||||
|
const customerProduct = customer.products.find((p) => p.id === pro.id);
|
||||||
|
const priceItem = customerProduct?.items?.find(isPriceItem);
|
||||||
|
expect(priceItem).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize attach: plan item v1 prepaid mapping")}`, async () => {
|
||||||
|
const customerId = "v2-attach-customize-prepaid-map";
|
||||||
|
|
||||||
|
const base = products.base({
|
||||||
|
id: "base",
|
||||||
|
items: [items.monthlyMessages({ includedUsage: 100 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [base] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: base.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
options: [{ feature_id: TestFeature.Messages, quantity: 100 }],
|
||||||
|
customize: {
|
||||||
|
price: null,
|
||||||
|
items: [itemsV2.prepaidMessages({ amount: 10, billingUnits: 100 })],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.billing.previewAttach<AttachParamsV1Input>(params);
|
||||||
|
expect(preview.total).toBe(10);
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 1,
|
||||||
|
latestTotal: 10,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize attach: plan item v1 multi-feature mapping")}`, async () => {
|
||||||
|
const customerId = "v2-attach-customize-multi-feature-map";
|
||||||
|
|
||||||
|
const base = products.base({
|
||||||
|
id: "base",
|
||||||
|
items: [items.monthlyMessages({ includedUsage: 100 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [base] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: base.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
customize: {
|
||||||
|
price: itemsV2.monthlyPrice({ amount: 40 }),
|
||||||
|
items: [
|
||||||
|
itemsV2.monthlyMessages({ included: 300 }),
|
||||||
|
itemsV2.monthlyWords({ included: 150 }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
includedUsage: 300,
|
||||||
|
balance: 300,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Words,
|
||||||
|
includedUsage: 150,
|
||||||
|
balance: 150,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize attach: paid feature mix (consumable + prepaid + allocated)")}`, async () => {
|
||||||
|
const customerId = "v2-attach-customize-paid-feature-mix";
|
||||||
|
|
||||||
|
const base = products.base({
|
||||||
|
id: "base",
|
||||||
|
items: [items.monthlyMessages({ includedUsage: 100 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [base] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: base.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
options: [{ feature_id: TestFeature.Words, quantity: 500 }],
|
||||||
|
customize: {
|
||||||
|
price: itemsV2.monthlyPrice({ amount: 40 }),
|
||||||
|
items: [
|
||||||
|
itemsV2.consumableMessages({ amount: 1 }),
|
||||||
|
itemsV2.prepaidWords({ amount: 15, billingUnits: 100, included: 200 }),
|
||||||
|
itemsV2.allocatedUsers({ amount: 10, included: 3 }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.billing.previewAttach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
// Base ($40) + prepaid words ($15 for 100 units) + allocated users at included quantity
|
||||||
|
expect(preview.total).toBe(85);
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Words,
|
||||||
|
includedUsage: 500,
|
||||||
|
balance: 500,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Users,
|
||||||
|
includedUsage: 3,
|
||||||
|
balance: 3,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectCustomerFeatureExists({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 1,
|
||||||
|
latestTotal: preview.total,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import type { ApiCustomerV3, AttachParamsV1Input } from "@autumn/shared";
|
||||||
|
import { FreeTrialDuration, ms } from "@autumn/shared";
|
||||||
|
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
|
||||||
|
import {
|
||||||
|
expectProductNotTrialing,
|
||||||
|
expectProductTrialing,
|
||||||
|
} from "@tests/integration/billing/utils/expectCustomerProductTrialing";
|
||||||
|
import { expectPreviewNextCycleCorrect } from "@tests/integration/billing/utils/expectPreviewNextCycleCorrect";
|
||||||
|
import { items } from "@tests/utils/fixtures/items";
|
||||||
|
import { products } from "@tests/utils/fixtures/products";
|
||||||
|
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { addMonths } from "date-fns";
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-free-trial attach: set trial with v1 free_trial params")}`, async () => {
|
||||||
|
const customerId = "v2-attach-free-trial-set";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2, advancedTo } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
free_trial: {
|
||||||
|
duration_length: 7,
|
||||||
|
duration_type: FreeTrialDuration.Day,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.billing.previewAttach<AttachParamsV1Input>(params);
|
||||||
|
expect(preview.total).toBe(0);
|
||||||
|
expectPreviewNextCycleCorrect({
|
||||||
|
preview,
|
||||||
|
startsAt: advancedTo + ms.days(7),
|
||||||
|
total: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductTrialing({
|
||||||
|
customer,
|
||||||
|
productId: pro.id,
|
||||||
|
trialEndsAt: advancedTo + ms.days(7),
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 1,
|
||||||
|
latestTotal: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-free-trial attach: remove product trial with free_trial null")}`, async () => {
|
||||||
|
const customerId = "v2-attach-free-trial-null";
|
||||||
|
|
||||||
|
const proTrial = products.proWithTrial({
|
||||||
|
id: "pro-trial",
|
||||||
|
items: [items.monthlyMessages({ includedUsage: 100 })],
|
||||||
|
trialDays: 14,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2, advancedTo } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [proTrial] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: proTrial.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
free_trial: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.billing.previewAttach<AttachParamsV1Input>(params);
|
||||||
|
expect(preview.total).toBe(20);
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductNotTrialing({
|
||||||
|
customer,
|
||||||
|
productId: proTrial.id,
|
||||||
|
nowMs: advancedTo,
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 1,
|
||||||
|
latestTotal: 20,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-free-trial attach: month-based v1 free_trial params")}`, async () => {
|
||||||
|
const customerId = "v2-attach-free-trial-month";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2, advancedTo } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: AttachParamsV1Input = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
free_trial: {
|
||||||
|
duration_length: 1,
|
||||||
|
duration_type: FreeTrialDuration.Month,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductTrialing({
|
||||||
|
customer,
|
||||||
|
productId: pro.id,
|
||||||
|
trialEndsAt: addMonths(advancedTo, 1).getTime(),
|
||||||
|
toleranceMs: ms.hours(2),
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import type {
|
||||||
|
ApiCustomerV3,
|
||||||
|
UpdateSubscriptionV1ParamsInput,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
|
||||||
|
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
|
||||||
|
import { TestFeature } from "@tests/setup/v2Features";
|
||||||
|
import { items } from "@tests/utils/fixtures/items";
|
||||||
|
import { itemsV2 } from "@tests/utils/fixtures/itemsV2";
|
||||||
|
import { products } from "@tests/utils/fixtures/products";
|
||||||
|
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize update: both customize.price + customize.items")}`, async () => {
|
||||||
|
const customerId = "v2-update-customize-both";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [s.billing.attach({ productId: pro.id })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: UpdateSubscriptionV1ParamsInput = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
customize: {
|
||||||
|
price: itemsV2.monthlyPrice({ amount: 30 }),
|
||||||
|
items: [itemsV2.monthlyWords({ included: 200 })],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.subscriptions.previewUpdate<UpdateSubscriptionV1ParamsInput>(
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
expect(preview.total).toBe(10);
|
||||||
|
|
||||||
|
await autumnV2.subscriptions.update<UpdateSubscriptionV1ParamsInput>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Words,
|
||||||
|
includedUsage: 200,
|
||||||
|
balance: 200,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 2,
|
||||||
|
latestTotal: 10,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize update: only customize.price")}`, async () => {
|
||||||
|
const customerId = "v2-update-customize-only-price";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [s.billing.attach({ productId: pro.id })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: UpdateSubscriptionV1ParamsInput = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
customize: {
|
||||||
|
price: itemsV2.monthlyPrice({ amount: 24 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.subscriptions.previewUpdate<UpdateSubscriptionV1ParamsInput>(
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
expect(preview.total).toBe(4);
|
||||||
|
|
||||||
|
await autumnV2.subscriptions.update<UpdateSubscriptionV1ParamsInput>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
includedUsage: 100,
|
||||||
|
balance: 100,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 2,
|
||||||
|
latestTotal: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize update: only customize.items")}`, async () => {
|
||||||
|
const customerId = "v2-update-customize-only-items";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [s.billing.attach({ productId: pro.id })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: UpdateSubscriptionV1ParamsInput = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
customize: {
|
||||||
|
price: null,
|
||||||
|
items: [itemsV2.monthlyMessages({ included: 180 })],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.subscriptions.previewUpdate<UpdateSubscriptionV1ParamsInput>(
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
expect(preview.total).toBe(-20);
|
||||||
|
|
||||||
|
await autumnV2.subscriptions.update<UpdateSubscriptionV1ParamsInput>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
includedUsage: 180,
|
||||||
|
balance: 180,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 2,
|
||||||
|
latestTotal: -20,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize update: plan item v1 prepaid mapping")}`, async () => {
|
||||||
|
const customerId = "v2-update-customize-prepaid-map";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [s.billing.attach({ productId: pro.id })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: UpdateSubscriptionV1ParamsInput = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
options: [{ feature_id: TestFeature.Messages, quantity: 100 }],
|
||||||
|
customize: {
|
||||||
|
price: null,
|
||||||
|
items: [itemsV2.prepaidMessages({ amount: 10, billingUnits: 100 })],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.subscriptions.previewUpdate<UpdateSubscriptionV1ParamsInput>(
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
expect(preview.total).toBe(-10);
|
||||||
|
|
||||||
|
await autumnV2.subscriptions.update<UpdateSubscriptionV1ParamsInput>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 2,
|
||||||
|
latestTotal: -10,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-customize update: plan item v1 multi-feature mapping")}`, async () => {
|
||||||
|
const customerId = "v2-update-customize-multi-feature-map";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2 } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [s.billing.attach({ productId: pro.id })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: UpdateSubscriptionV1ParamsInput = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
customize: {
|
||||||
|
price: itemsV2.monthlyPrice({ amount: 40 }),
|
||||||
|
items: [
|
||||||
|
itemsV2.monthlyMessages({ included: 300 }),
|
||||||
|
itemsV2.monthlyWords({ included: 150 }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await autumnV2.subscriptions.update<UpdateSubscriptionV1ParamsInput>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
includedUsage: 300,
|
||||||
|
balance: 300,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Words,
|
||||||
|
includedUsage: 150,
|
||||||
|
balance: 150,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import type {
|
||||||
|
ApiCustomerV3,
|
||||||
|
UpdateSubscriptionV1ParamsInput,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { FreeTrialDuration, ms } from "@autumn/shared";
|
||||||
|
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
|
||||||
|
import {
|
||||||
|
expectProductNotTrialing,
|
||||||
|
expectProductTrialing,
|
||||||
|
} from "@tests/integration/billing/utils/expectCustomerProductTrialing";
|
||||||
|
import { expectPreviewNextCycleCorrect } from "@tests/integration/billing/utils/expectPreviewNextCycleCorrect";
|
||||||
|
import { items } from "@tests/utils/fixtures/items";
|
||||||
|
import { products } from "@tests/utils/fixtures/products";
|
||||||
|
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { addMonths } from "date-fns";
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-free-trial update: set trial with v1 free_trial params")}`, async () => {
|
||||||
|
const customerId = "v2-update-free-trial-set";
|
||||||
|
|
||||||
|
const pro = products.base({
|
||||||
|
id: "pro",
|
||||||
|
items: [
|
||||||
|
items.monthlyMessages({ includedUsage: 100 }),
|
||||||
|
items.monthlyPrice({ price: 20 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2, advancedTo } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success", testClock: true }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [s.billing.attach({ productId: pro.id })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: UpdateSubscriptionV1ParamsInput = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
free_trial: {
|
||||||
|
duration_length: 7,
|
||||||
|
duration_type: FreeTrialDuration.Day,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.subscriptions.previewUpdate<UpdateSubscriptionV1ParamsInput>(
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
expect(preview.total).toBe(-20);
|
||||||
|
expectPreviewNextCycleCorrect({
|
||||||
|
preview,
|
||||||
|
startsAt: advancedTo + ms.days(7),
|
||||||
|
total: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
await autumnV2.subscriptions.update<UpdateSubscriptionV1ParamsInput>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductTrialing({
|
||||||
|
customer,
|
||||||
|
productId: pro.id,
|
||||||
|
trialEndsAt: advancedTo + ms.days(7),
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 2,
|
||||||
|
latestTotal: -20,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-free-trial update: remove trial with free_trial null")}`, async () => {
|
||||||
|
const customerId = "v2-update-free-trial-remove";
|
||||||
|
|
||||||
|
const proTrial = products.proWithTrial({
|
||||||
|
id: "pro-trial",
|
||||||
|
items: [items.monthlyMessages({ includedUsage: 100 })],
|
||||||
|
trialDays: 14,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2, advancedTo } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success", testClock: true }),
|
||||||
|
s.products({ list: [proTrial] }),
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
s.billing.attach({ productId: proTrial.id }),
|
||||||
|
s.advanceTestClock({ days: 3 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: UpdateSubscriptionV1ParamsInput = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: proTrial.id,
|
||||||
|
free_trial: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
await autumnV2.subscriptions.previewUpdate<UpdateSubscriptionV1ParamsInput>(
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
expect(preview.total).toBe(20);
|
||||||
|
|
||||||
|
await autumnV2.subscriptions.update<UpdateSubscriptionV1ParamsInput>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductNotTrialing({
|
||||||
|
customer,
|
||||||
|
productId: proTrial.id,
|
||||||
|
nowMs: advancedTo,
|
||||||
|
});
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 2,
|
||||||
|
latestTotal: 20,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent(`${chalk.yellowBright("v2-free-trial update: replace active trial with month trial")}`, async () => {
|
||||||
|
const customerId = "v2-update-free-trial-replace";
|
||||||
|
|
||||||
|
const proTrial = products.proWithTrial({
|
||||||
|
id: "pro-trial",
|
||||||
|
items: [items.monthlyMessages({ includedUsage: 100 })],
|
||||||
|
trialDays: 7,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, autumnV2, advancedTo } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success", testClock: true }),
|
||||||
|
s.products({ list: [proTrial] }),
|
||||||
|
],
|
||||||
|
actions: [s.billing.attach({ productId: proTrial.id })],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params: UpdateSubscriptionV1ParamsInput = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: proTrial.id,
|
||||||
|
free_trial: {
|
||||||
|
duration_length: 1,
|
||||||
|
duration_type: FreeTrialDuration.Month,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await autumnV2.subscriptions.update<UpdateSubscriptionV1ParamsInput>(params);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectProductTrialing({
|
||||||
|
customer,
|
||||||
|
productId: proTrial.id,
|
||||||
|
trialEndsAt: addMonths(advancedTo, 1).getTime(),
|
||||||
|
toleranceMs: ms.hours(2),
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
ApiVersion,
|
ApiVersion,
|
||||||
formatMs,
|
formatMs,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import type { Customer } from "autumn-js";
|
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli";
|
import { AutumnInt } from "@/external/autumn/autumnCli";
|
||||||
|
|
||||||
const defaultAutumn = new AutumnInt({ version: ApiVersion.V1_2 });
|
const defaultAutumn = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||||
@@ -16,7 +15,7 @@ export const expectCustomerFeatureExists = async ({
|
|||||||
featureId,
|
featureId,
|
||||||
}: {
|
}: {
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
customer?: Customer | ApiEntityV0;
|
customer?: ApiCustomerV3 | ApiEntityV0;
|
||||||
featureId: string;
|
featureId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const customer = providedCustomer
|
const customer = providedCustomer
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { ExistingRollover } from "@autumn/shared";
|
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@@ -16,6 +16,7 @@ describe(chalk.yellowBright("applyExistingRollovers"), () => {
|
|||||||
featureName: "Words",
|
featureName: "Words",
|
||||||
allowance: 5000,
|
allowance: 5000,
|
||||||
balance: 5000,
|
balance: 5000,
|
||||||
|
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCusProduct = customerProducts.create({
|
const newCusProduct = customerProducts.create({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { ExistingRollover } from "@autumn/shared";
|
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@@ -19,6 +19,7 @@ describe(
|
|||||||
featureName: "Words",
|
featureName: "Words",
|
||||||
allowance: 5000,
|
allowance: 5000,
|
||||||
balance: 5000,
|
balance: 5000,
|
||||||
|
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCusProduct = customerProducts.create({
|
const newCusProduct = customerProducts.create({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { ExistingRollover } from "@autumn/shared";
|
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@@ -15,6 +15,7 @@ describe(
|
|||||||
featureName: "Feature A",
|
featureName: "Feature A",
|
||||||
allowance: 100,
|
allowance: 100,
|
||||||
balance: 100,
|
balance: 100,
|
||||||
|
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const cusEntB = customerEntitlements.create({
|
const cusEntB = customerEntitlements.create({
|
||||||
@@ -23,6 +24,7 @@ describe(
|
|||||||
featureName: "Feature B",
|
featureName: "Feature B",
|
||||||
allowance: 200,
|
allowance: 200,
|
||||||
balance: 200,
|
balance: 200,
|
||||||
|
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCusProduct = customerProducts.create({
|
const newCusProduct = customerProducts.create({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { ExistingRollover } from "@autumn/shared";
|
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@@ -18,6 +18,7 @@ describe(
|
|||||||
featureName: "Words",
|
featureName: "Words",
|
||||||
allowance: 100,
|
allowance: 100,
|
||||||
balance: 100,
|
balance: 100,
|
||||||
|
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const cusEntSecond = customerEntitlements.create({
|
const cusEntSecond = customerEntitlements.create({
|
||||||
@@ -27,6 +28,7 @@ describe(
|
|||||||
featureName: "Words",
|
featureName: "Words",
|
||||||
allowance: 200,
|
allowance: 200,
|
||||||
balance: 200,
|
balance: 200,
|
||||||
|
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCusProduct = customerProducts.create({
|
const newCusProduct = customerProducts.create({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { ExistingRollover } from "@autumn/shared";
|
import { type ExistingRollover, RolloverExpiryDurationType } from "@autumn/shared";
|
||||||
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements";
|
||||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@@ -19,6 +19,7 @@ describe(
|
|||||||
featureName: "Seats",
|
featureName: "Seats",
|
||||||
allowance: 10,
|
allowance: 10,
|
||||||
balance: 10,
|
balance: 10,
|
||||||
|
rollover: { max: null, duration: RolloverExpiryDurationType.Month, length: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCusProduct = customerProducts.create({
|
const newCusProduct = customerProducts.create({
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 150: 100 from included + 50 from overage
|
// Usage should be 150: 100 from included + 50 from overage
|
||||||
// Formula: usage = includedUsage - balance = 100 - (-50) = 150
|
// Formula: usage = includedUsage - balance = 100 - (-50) = 150
|
||||||
@@ -83,7 +83,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 70: allowance 100 - balance 30 = 70
|
// Usage should be 70: allowance 100 - balance 30 = 70
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -121,7 +121,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 200: all from overage (0 - (-200) = 200)
|
// Usage should be 200: all from overage (0 - (-200) = 200)
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -158,7 +158,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 100: exactly at the limit
|
// Usage should be 100: exactly at the limit
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -201,7 +201,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 3: 5 included - 2 remaining = 3 used
|
// Usage should be 3: 5 included - 2 remaining = 3 used
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -238,7 +238,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 5: 3 - (-2) = 5 total seats used
|
// Usage should be 5: 3 - (-2) = 5 total seats used
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -275,7 +275,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 10: 0 - (-10) = 10
|
// Usage should be 10: 0 - (-10) = 10
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -320,7 +320,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// For prepaid, usage = purchasedQuantity - balance
|
// For prepaid, usage = purchasedQuantity - balance
|
||||||
// purchasedQuantity from 10 billing units = 1000
|
// purchasedQuantity from 10 billing units = 1000
|
||||||
@@ -362,7 +362,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -399,7 +399,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
// Overage should be captured
|
// Overage should be captured
|
||||||
@@ -471,7 +471,7 @@ describe(
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Messages: 100 - (-20) = 120 used
|
// Messages: 100 - (-20) = 120 used
|
||||||
expect(existingUsages[messagesFeatureId]).toBeDefined();
|
expect(existingUsages[messagesFeatureId]).toBeDefined();
|
||||||
@@ -550,7 +550,7 @@ describe(
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Messages: 50 - (-150) = 200 used
|
// Messages: 50 - (-150) = 200 used
|
||||||
expect(existingUsages[messagesFeatureId]).toBeDefined();
|
expect(existingUsages[messagesFeatureId]).toBeDefined();
|
||||||
@@ -601,7 +601,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 0
|
// Usage should be 0
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -638,7 +638,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 10100: 100 - (-10000) = 10100
|
// Usage should be 10100: 100 - (-10000) = 10100
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -675,7 +675,7 @@ describe(
|
|||||||
customerPrices: [customerPrice],
|
customerPrices: [customerPrice],
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Usage should be 150.5
|
// Usage should be 150.5
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe(chalk.yellowBright("cusProductToExistingUsages"), () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Assert: Total usage should be 40 (20 + 20)
|
// Assert: Total usage should be 40 (20 + 20)
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -78,7 +78,7 @@ describe(chalk.yellowBright("cusProductToExistingUsages"), () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(existingUsages[internalFeatureId]).toBeDefined();
|
expect(existingUsages[internalFeatureId]).toBeDefined();
|
||||||
@@ -130,7 +130,7 @@ describe(chalk.yellowBright("cusProductToExistingUsages"), () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const existingUsages = cusProductToExistingUsages({ cusProduct });
|
const existingUsages = cusProductToExistingUsages({ cusProduct, carryAllConsumableFeatures: true });
|
||||||
|
|
||||||
// Assert: Usage should be 20 (allowance 100 - balance 80)
|
// Assert: Usage should be 20 (allowance 100 - balance 80)
|
||||||
// NOT 50 (allowance 100 + rollover 30 - balance 80)
|
// NOT 50 (allowance 100 + rollover 30 - balance 80)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { UpdateSubscriptionV0Params } from "@autumn/shared";
|
import type { UpdateSubscriptionV1Params } from "@autumn/shared";
|
||||||
import { contexts } from "@tests/utils/fixtures/db/contexts";
|
import { contexts } from "@tests/utils/fixtures/db/contexts";
|
||||||
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts";
|
||||||
|
import { entitlements } from "@tests/utils/fixtures/db/entitlements";
|
||||||
import { features } from "@tests/utils/fixtures/db/features";
|
import { features } from "@tests/utils/fixtures/db/features";
|
||||||
import { prices } from "@tests/utils/fixtures/db/prices";
|
import { prices } from "@tests/utils/fixtures/db/prices";
|
||||||
import { products } from "@tests/utils/fixtures/db/products";
|
import { products } from "@tests/utils/fixtures/db/products";
|
||||||
@@ -18,12 +19,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
@@ -35,7 +47,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
customerPrices: [prices.createCustomer({ price })],
|
customerPrices: [prices.createCustomer({ price })],
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
// No options provided
|
// No options provided
|
||||||
@@ -61,15 +73,26 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({ options: [] });
|
const cusProduct = customerProducts.create({ options: [] });
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
options: [{ feature_id: "credits", quantity: 50 }],
|
options: [{ feature_id: "credits", quantity: 50 }],
|
||||||
@@ -95,12 +118,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
@@ -111,7 +145,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
options: [{ feature_id: "credits", quantity: 200 }],
|
options: [{ feature_id: "credits", quantity: 200 }],
|
||||||
@@ -147,21 +181,44 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Storage",
|
name: "Storage",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const creditsEnt = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
const seatsEnt = entitlements.create({
|
||||||
|
id: "ent_seats",
|
||||||
|
featureId: "seats",
|
||||||
|
featureName: "Seats",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
const storageEnt = entitlements.create({
|
||||||
|
id: "ent_storage",
|
||||||
|
featureId: "storage",
|
||||||
|
featureName: "Storage",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const creditsPrice = prices.createPrepaid({
|
const creditsPrice = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
const seatsPrice = prices.createPrepaid({
|
const seatsPrice = prices.createPrepaid({
|
||||||
id: "price_seats",
|
id: "price_seats",
|
||||||
featureId: "seats",
|
featureId: "seats",
|
||||||
|
entitlementId: "ent_seats",
|
||||||
});
|
});
|
||||||
const storagePrice = prices.createPrepaid({
|
const storagePrice = prices.createPrepaid({
|
||||||
id: "price_storage",
|
id: "price_storage",
|
||||||
featureId: "storage",
|
featureId: "storage",
|
||||||
|
entitlementId: "ent_storage",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({
|
const fullProduct = products.createFull({
|
||||||
prices: [creditsPrice, seatsPrice, storagePrice],
|
prices: [creditsPrice, seatsPrice, storagePrice],
|
||||||
|
entitlements: [creditsEnt, seatsEnt, storageEnt],
|
||||||
});
|
});
|
||||||
|
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
@@ -189,7 +246,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
options: [{ feature_id: "seats", quantity: 10 }], // Only updating seats
|
options: [{ feature_id: "seats", quantity: 10 }], // Only updating seats
|
||||||
@@ -225,16 +282,27 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
billingUnits: 100, // Billing in units of 100
|
billingUnits: 100, // Billing in units of 100
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({ options: [] });
|
const cusProduct = customerProducts.create({ options: [] });
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
options: [{ feature_id: "credits", quantity: 150 }], // Should round up to 200
|
options: [{ feature_id: "credits", quantity: 150 }], // Should round up to 200
|
||||||
@@ -261,16 +329,27 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
billingUnits: 50,
|
billingUnits: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({ options: [] });
|
const cusProduct = customerProducts.create({ options: [] });
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
options: [{ feature_id: "credits", quantity: 200 }], // Exact multiple
|
options: [{ feature_id: "credits", quantity: 200 }], // Exact multiple
|
||||||
@@ -296,16 +375,27 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
billingUnits: 1000,
|
billingUnits: 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({ options: [] });
|
const cusProduct = customerProducts.create({ options: [] });
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
options: [{ feature_id: "credits", quantity: 1 }], // Should round to 1000
|
options: [{ feature_id: "credits", quantity: 1 }], // Should round to 1000
|
||||||
@@ -334,10 +424,18 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
// Old price: billing units of 100
|
// Old price: billing units of 100
|
||||||
const oldPrice = prices.createPrepaid({
|
const oldPrice = prices.createPrepaid({
|
||||||
id: "price_credits_old",
|
id: "price_credits_old",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
billingUnits: 100,
|
billingUnits: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -345,10 +443,14 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
const newPrice = prices.createPrepaid({
|
const newPrice = prices.createPrepaid({
|
||||||
id: "price_credits_new",
|
id: "price_credits_new",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
billingUnits: 250,
|
billingUnits: 250,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [newPrice] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [newPrice],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
|
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
options: [
|
options: [
|
||||||
@@ -361,7 +463,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
customerPrices: [prices.createCustomer({ price: oldPrice })],
|
customerPrices: [prices.createCustomer({ price: oldPrice })],
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
// No options - should inherit from current
|
// No options - should inherit from current
|
||||||
@@ -393,19 +495,31 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const oldPrice = prices.createPrepaid({
|
const oldPrice = prices.createPrepaid({
|
||||||
id: "price_credits_old",
|
id: "price_credits_old",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
billingUnits: 100,
|
billingUnits: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newPrice = prices.createPrepaid({
|
const newPrice = prices.createPrepaid({
|
||||||
id: "price_credits_new",
|
id: "price_credits_new",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
billingUnits: 250,
|
billingUnits: 250,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [newPrice] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [newPrice],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
|
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
options: [
|
options: [
|
||||||
@@ -418,7 +532,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
customerPrices: [prices.createCustomer({ price: oldPrice })],
|
customerPrices: [prices.createCustomer({ price: oldPrice })],
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
};
|
};
|
||||||
@@ -447,13 +561,24 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const newPrice = prices.createPrepaid({
|
const newPrice = prices.createPrepaid({
|
||||||
id: "price_credits_new",
|
id: "price_credits_new",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
billingUnits: 250,
|
billingUnits: 250,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [newPrice] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [newPrice],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
|
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
options: [
|
options: [
|
||||||
@@ -466,7 +591,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
// No customerPrices - can't interpret stored quantity
|
// No customerPrices - can't interpret stored quantity
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
};
|
};
|
||||||
@@ -493,19 +618,28 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const fixedPrice = prices.createFixed({ id: "price_fixed" });
|
const fixedPrice = prices.createFixed({ id: "price_fixed" });
|
||||||
const prepaidPrice = prices.createPrepaid({
|
const prepaidPrice = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({
|
const fullProduct = products.createFull({
|
||||||
prices: [fixedPrice, prepaidPrice],
|
prices: [fixedPrice, prepaidPrice],
|
||||||
|
entitlements: [entitlement],
|
||||||
});
|
});
|
||||||
|
|
||||||
const cusProduct = customerProducts.create({ options: [] });
|
const cusProduct = customerProducts.create({ options: [] });
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
options: [{ feature_id: "credits", quantity: 50 }],
|
options: [{ feature_id: "credits", quantity: 50 }],
|
||||||
@@ -528,10 +662,10 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
|
|
||||||
describe("edge cases", () => {
|
describe("edge cases", () => {
|
||||||
test("empty prices array returns empty array", () => {
|
test("empty prices array returns empty array", () => {
|
||||||
const fullProduct = products.createFull({ prices: [] });
|
const fullProduct = products.createFull({ prices: [], entitlements: [] });
|
||||||
const cusProduct = customerProducts.create({ options: [] });
|
const cusProduct = customerProducts.create({ options: [] });
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
};
|
};
|
||||||
@@ -548,21 +682,25 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
expect(result).toHaveLength(0);
|
expect(result).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws error when feature not found for price", () => {
|
test("throws error when entitlement not found for price", () => {
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
// No entitlements provided - entitlement won't be found
|
||||||
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({ options: [] });
|
const cusProduct = customerProducts.create({ options: [] });
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Empty features array - feature won't be found
|
|
||||||
const ctx = contexts.create({ features: [] });
|
const ctx = contexts.create({ features: [] });
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
@@ -572,7 +710,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
fullProduct,
|
fullProduct,
|
||||||
currentCustomerProduct: cusProduct,
|
currentCustomerProduct: cusProduct,
|
||||||
}),
|
}),
|
||||||
).toThrow("Feature not found for price");
|
).toThrow("Entitlement not found for price");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("neither current nor new has quantity → feature not included", () => {
|
test("neither current nor new has quantity → feature not included", () => {
|
||||||
@@ -581,15 +719,26 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({ options: [] }); // No current options
|
const cusProduct = customerProducts.create({ options: [] }); // No current options
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
// No options in params either
|
// No options in params either
|
||||||
@@ -614,12 +763,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
@@ -631,7 +791,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
customerPrices: [prices.createCustomer({ price })],
|
customerPrices: [prices.createCustomer({ price })],
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
options: [], // Explicitly empty
|
options: [], // Explicitly empty
|
||||||
@@ -657,12 +817,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
@@ -674,7 +845,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
customerPrices: [prices.createCustomer({ price })],
|
customerPrices: [prices.createCustomer({ price })],
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
options: [{ feature_id: "credits", quantity: 0 }],
|
options: [{ feature_id: "credits", quantity: 0 }],
|
||||||
@@ -700,13 +871,25 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Credits",
|
name: "Credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_credits",
|
||||||
|
featureId: "credits",
|
||||||
|
internalFeatureId: "internal_credits_v2",
|
||||||
|
featureName: "Credits",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_credits",
|
id: "price_credits",
|
||||||
featureId: "credits",
|
featureId: "credits",
|
||||||
internalFeatureId: "internal_credits_v2",
|
internalFeatureId: "internal_credits_v2",
|
||||||
|
entitlementId: "ent_credits",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
@@ -718,7 +901,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
customerPrices: [prices.createCustomer({ price })],
|
customerPrices: [prices.createCustomer({ price })],
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
};
|
};
|
||||||
@@ -742,12 +925,23 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
name: "Seats",
|
name: "Seats",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const entitlement = entitlements.create({
|
||||||
|
id: "ent_seats",
|
||||||
|
featureId: "seats",
|
||||||
|
featureName: "Seats",
|
||||||
|
allowance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const price = prices.createPrepaid({
|
const price = prices.createPrepaid({
|
||||||
id: "price_seats",
|
id: "price_seats",
|
||||||
featureId: "seats",
|
featureId: "seats",
|
||||||
|
entitlementId: "ent_seats",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullProduct = products.createFull({ prices: [price] });
|
const fullProduct = products.createFull({
|
||||||
|
prices: [price],
|
||||||
|
entitlements: [entitlement],
|
||||||
|
});
|
||||||
const cusProduct = customerProducts.create({
|
const cusProduct = customerProducts.create({
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
@@ -759,7 +953,7 @@ describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
|||||||
customerPrices: [prices.createCustomer({ price })],
|
customerPrices: [prices.createCustomer({ price })],
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
// No options provided - should carry over from current
|
// No options provided - should carry over from current
|
||||||
|
|||||||
@@ -180,15 +180,15 @@ export const createCustomerPricesForProduct = ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all stripe price IDs from a product helper result.
|
* Gets all stripe price IDs from a product helper result.
|
||||||
* Entity-level products use stripeEmptyPriceId for consumable prices.
|
* Note: As of current implementation, withEntity is hardcoded to false in customerProductToStripeItemSpecs,
|
||||||
|
* so consumable prices always use the regular (non-empty) price ID.
|
||||||
*/
|
*/
|
||||||
export const getStripePriceIds = (
|
export const getStripePriceIds = (
|
||||||
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
||||||
{ isEntityLevel = false }: { isEntityLevel?: boolean } = {},
|
{ isEntityLevel: _isEntityLevel = false }: { isEntityLevel?: boolean } = {},
|
||||||
): string[] => {
|
): string[] => {
|
||||||
const consumablePriceId = isEntityLevel
|
// withEntity is hardcoded to false, so always use regular consumable price
|
||||||
? `stripe_${product.product.id}_consumable_empty`
|
const consumablePriceId = `stripe_${product.product.id}_consumable`;
|
||||||
: `stripe_${product.product.id}_consumable`;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`stripe_${product.product.id}_fixed`,
|
`stripe_${product.product.id}_fixed`,
|
||||||
@@ -232,21 +232,16 @@ export const expectPhaseItems = (
|
|||||||
export const getExpectedPhaseItems = (
|
export const getExpectedPhaseItems = (
|
||||||
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
||||||
{
|
{
|
||||||
isEntityLevel = false,
|
isEntityLevel: _isEntityLevel = false,
|
||||||
fixedQuantityMultiplier = 1,
|
fixedQuantityMultiplier = 1,
|
||||||
}: { isEntityLevel?: boolean; fixedQuantityMultiplier?: number } = {},
|
}: { isEntityLevel?: boolean; fixedQuantityMultiplier?: number } = {},
|
||||||
): ExpectedPhaseItem[] => {
|
): ExpectedPhaseItem[] => {
|
||||||
const productId = product.product.id;
|
const productId = product.product.id;
|
||||||
const { expectedQuantities } = product;
|
const { expectedQuantities } = product;
|
||||||
|
|
||||||
const consumablePriceId = isEntityLevel
|
// withEntity is hardcoded to false, so always use regular consumable price
|
||||||
? `stripe_${productId}_consumable_empty`
|
const consumablePriceId = `stripe_${productId}_consumable`;
|
||||||
: `stripe_${productId}_consumable`;
|
const consumableQuantity = expectedQuantities.consumable;
|
||||||
|
|
||||||
// Entity consumable uses empty price with quantity 0
|
|
||||||
const consumableQuantity = isEntityLevel
|
|
||||||
? expectedQuantities.consumableEntity
|
|
||||||
: expectedQuantities.consumable;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -298,24 +293,19 @@ export type ExpectedSubscriptionItemUpdate = {
|
|||||||
* Gets expected subscription item updates for creating a new product.
|
* Gets expected subscription item updates for creating a new product.
|
||||||
* Uses the expectedQuantities from the product helper.
|
* Uses the expectedQuantities from the product helper.
|
||||||
*
|
*
|
||||||
* @param isEntityLevel - If true, uses stripeEmptyPriceId for consumable (quantity 0)
|
* Note: As of current implementation, withEntity is hardcoded to false in customerProductToStripeItemSpecs,
|
||||||
|
* so consumable prices always use the regular (non-empty) price ID.
|
||||||
*/
|
*/
|
||||||
export const getExpectedNewProductItems = (
|
export const getExpectedNewProductItems = (
|
||||||
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
||||||
{ isEntityLevel = false }: { isEntityLevel?: boolean } = {},
|
{ isEntityLevel: _isEntityLevel = false }: { isEntityLevel?: boolean } = {},
|
||||||
): ExpectedSubscriptionItemUpdate[] => {
|
): ExpectedSubscriptionItemUpdate[] => {
|
||||||
const productId = product.product.id;
|
const productId = product.product.id;
|
||||||
const { expectedQuantities } = product;
|
const { expectedQuantities } = product;
|
||||||
|
|
||||||
const consumablePriceId = isEntityLevel
|
// withEntity is hardcoded to false, so always use regular consumable price (metered, no quantity)
|
||||||
? `stripe_${productId}_consumable_empty`
|
const consumablePriceId = `stripe_${productId}_consumable`;
|
||||||
: `stripe_${productId}_consumable`;
|
const consumableQuantity = expectedQuantities.consumable;
|
||||||
|
|
||||||
// Entity consumable uses empty price with quantity 0
|
|
||||||
// Customer consumable is metered (no quantity)
|
|
||||||
const consumableQuantity = isEntityLevel
|
|
||||||
? expectedQuantities.consumableEntity
|
|
||||||
: expectedQuantities.consumable;
|
|
||||||
|
|
||||||
const items: ExpectedSubscriptionItemUpdate[] = [
|
const items: ExpectedSubscriptionItemUpdate[] = [
|
||||||
{
|
{
|
||||||
@@ -370,17 +360,17 @@ export const expectSubscriptionItemsUpdate = (
|
|||||||
* Creates stripe subscription items from product's expected prices.
|
* Creates stripe subscription items from product's expected prices.
|
||||||
* Useful for setting up existing subscription state.
|
* Useful for setting up existing subscription state.
|
||||||
*
|
*
|
||||||
* Note: For customer-level (non-entity) products, metered consumable prices
|
* Note: As of current implementation, withEntity is hardcoded to false in customerProductToStripeItemSpecs,
|
||||||
* are excluded because they don't have a meaningful quantity to compare.
|
* so consumable prices always use the regular (non-empty) price ID. Metered consumable prices
|
||||||
* Entity-level products use the empty price with quantity 0.
|
* are excluded by default because they don't have a meaningful quantity to compare.
|
||||||
*
|
*
|
||||||
* @param includeMetered - If true, includes metered consumable even for customer-level.
|
* @param includeMetered - If true, includes metered consumable. Use this when testing scenarios
|
||||||
* Use this when testing scenarios that need the full item set.
|
* that need the full item set.
|
||||||
*/
|
*/
|
||||||
export const createStripeItemsFromProduct = (
|
export const createStripeItemsFromProduct = (
|
||||||
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
product: ReturnType<typeof createProductWithAllPriceTypes>,
|
||||||
{
|
{
|
||||||
isEntityLevel = false,
|
isEntityLevel: _isEntityLevel = false,
|
||||||
itemIdPrefix = "si",
|
itemIdPrefix = "si",
|
||||||
includeMetered = false,
|
includeMetered = false,
|
||||||
}: {
|
}: {
|
||||||
@@ -410,15 +400,9 @@ export const createStripeItemsFromProduct = (
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// For entity-level, always include empty price with quantity 0
|
// Only include metered if explicitly requested (withEntity is hardcoded to false,
|
||||||
// For customer-level, only include metered if explicitly requested
|
// so always use regular consumable price)
|
||||||
if (isEntityLevel) {
|
if (includeMetered) {
|
||||||
items.push({
|
|
||||||
id: `${itemIdPrefix}_${productId}_consumable`,
|
|
||||||
priceId: `stripe_${productId}_consumable_empty`,
|
|
||||||
quantity: expectedQuantities.consumableEntity ?? 0,
|
|
||||||
});
|
|
||||||
} else if (includeMetered) {
|
|
||||||
items.push({
|
items.push({
|
||||||
id: `${itemIdPrefix}_${productId}_consumable`,
|
id: `${itemIdPrefix}_${productId}_consumable`,
|
||||||
priceId: `stripe_${productId}_consumable`,
|
priceId: `stripe_${productId}_consumable`,
|
||||||
|
|||||||
@@ -105,12 +105,13 @@ describe(
|
|||||||
);
|
);
|
||||||
expect(prepaidItem?.quantity).toBe(200);
|
expect(prepaidItem?.quantity).toBe(200);
|
||||||
|
|
||||||
// Consumable (entities use empty price): 0 + 0 = 0
|
// Consumable (metered, withEntity hardcoded to false): no quantity
|
||||||
const consumableItem = result.find((item) =>
|
const consumableItem = result.find((item) =>
|
||||||
item.price?.includes("consumable"),
|
item.price?.includes("consumable"),
|
||||||
);
|
);
|
||||||
expect(consumableItem?.price).toBe("stripe_pro_consumable_empty");
|
expect(consumableItem?.price).toBe("stripe_pro_consumable");
|
||||||
expect(consumableItem?.quantity).toBe(0);
|
// Metered prices should NOT have quantity
|
||||||
|
expect("quantity" in (consumableItem ?? {})).toBe(false);
|
||||||
|
|
||||||
// Allocated: 5 + 5 = 10
|
// Allocated: 5 + 5 = 10
|
||||||
const allocatedItem = result.find((item) =>
|
const allocatedItem = result.find((item) =>
|
||||||
@@ -293,8 +294,8 @@ describe(
|
|||||||
finalCustomerProducts: [entity1ProProduct, entity2ProProduct],
|
finalCustomerProducts: [entity1ProProduct, entity2ProProduct],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should update quantities (double them)
|
// Should update quantities (double them) + add metered consumable
|
||||||
expect(result).toHaveLength(3); // fixed, prepaid, allocated (consumable stays 0)
|
expect(result).toHaveLength(4); // fixed, prepaid, allocated, consumable (metered)
|
||||||
|
|
||||||
const fixedItem = result.find((item) => item.id?.includes("fixed"));
|
const fixedItem = result.find((item) => item.id?.includes("fixed"));
|
||||||
expect(fixedItem?.quantity).toBe(2);
|
expect(fixedItem?.quantity).toBe(2);
|
||||||
@@ -306,6 +307,12 @@ describe(
|
|||||||
item.id?.includes("allocated"),
|
item.id?.includes("allocated"),
|
||||||
);
|
);
|
||||||
expect(allocatedItem?.quantity).toBe(10);
|
expect(allocatedItem?.quantity).toBe(10);
|
||||||
|
|
||||||
|
// Metered consumable needs to be added
|
||||||
|
const consumableItem = result.find((item) =>
|
||||||
|
item.price?.includes("consumable"),
|
||||||
|
);
|
||||||
|
expect(consumableItem).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Remove one entity from two-entity subscription", () => {
|
test("Remove one entity from two-entity subscription", () => {
|
||||||
@@ -334,7 +341,7 @@ describe(
|
|||||||
subscriptionIds: ["sub_123"],
|
subscriptionIds: ["sub_123"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Current subscription has 2 entities worth of quantities
|
// Current subscription has 2 entities worth of quantities (no consumable since withEntity is false)
|
||||||
const stripeSubscription = stripeSubscriptions.create({
|
const stripeSubscription = stripeSubscriptions.create({
|
||||||
id: "sub_123",
|
id: "sub_123",
|
||||||
items: [
|
items: [
|
||||||
@@ -348,11 +355,6 @@ describe(
|
|||||||
priceId: "stripe_pro_prepaid",
|
priceId: "stripe_pro_prepaid",
|
||||||
quantity: 200,
|
quantity: 200,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "si_pro_consumable",
|
|
||||||
priceId: "stripe_pro_consumable_empty",
|
|
||||||
quantity: 0,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "si_pro_allocated",
|
id: "si_pro_allocated",
|
||||||
priceId: "stripe_pro_allocated",
|
priceId: "stripe_pro_allocated",
|
||||||
@@ -373,8 +375,8 @@ describe(
|
|||||||
finalCustomerProducts: [entity1ProProduct],
|
finalCustomerProducts: [entity1ProProduct],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should update quantities (halve them)
|
// Should update quantities (halve them) + add metered consumable
|
||||||
expect(result).toHaveLength(3); // fixed, prepaid, allocated
|
expect(result).toHaveLength(4); // fixed, prepaid, allocated, consumable
|
||||||
|
|
||||||
const fixedItem = result.find((item) => item.id?.includes("fixed"));
|
const fixedItem = result.find((item) => item.id?.includes("fixed"));
|
||||||
expect(fixedItem?.quantity).toBe(1);
|
expect(fixedItem?.quantity).toBe(1);
|
||||||
@@ -386,6 +388,12 @@ describe(
|
|||||||
item.id?.includes("allocated"),
|
item.id?.includes("allocated"),
|
||||||
);
|
);
|
||||||
expect(allocatedItem?.quantity).toBe(5);
|
expect(allocatedItem?.quantity).toBe(5);
|
||||||
|
|
||||||
|
// Metered consumable needs to be added
|
||||||
|
const consumableItem = result.find((item) =>
|
||||||
|
item.price?.includes("consumable"),
|
||||||
|
);
|
||||||
|
expect(consumableItem).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -524,13 +532,12 @@ describe(
|
|||||||
finalCustomerProducts: [customerProProduct, entityProProduct],
|
finalCustomerProducts: [customerProProduct, entityProProduct],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should have 5 items:
|
// Should have 4 items (withEntity hardcoded to false, so both use metered):
|
||||||
// - fixed (1+1=2)
|
// - fixed (1+1=2)
|
||||||
// - prepaid (100+50=150)
|
// - prepaid (100+50=150)
|
||||||
// - allocated (5+3=8)
|
// - allocated (5+3=8)
|
||||||
// - consumable metered (customer)
|
// - consumable metered (both customer and entity use regular price)
|
||||||
// - consumable empty (entity)
|
expect(result).toHaveLength(4);
|
||||||
expect(result).toHaveLength(5);
|
|
||||||
|
|
||||||
// Fixed: 1 + 1 = 2
|
// Fixed: 1 + 1 = 2
|
||||||
const fixedItem = result.find((item) => item.price?.includes("fixed"));
|
const fixedItem = result.find((item) => item.price?.includes("fixed"));
|
||||||
@@ -548,16 +555,11 @@ describe(
|
|||||||
);
|
);
|
||||||
expect(allocatedItem?.quantity).toBe(8);
|
expect(allocatedItem?.quantity).toBe(8);
|
||||||
|
|
||||||
// Both consumable prices should be present
|
// Only one consumable price (metered, withEntity is false)
|
||||||
const consumableMetered = result.find(
|
const consumableMetered = result.find(
|
||||||
(item) => item.price === "stripe_pro_consumable",
|
(item) => item.price === "stripe_pro_consumable",
|
||||||
);
|
);
|
||||||
const consumableEmpty = result.find(
|
|
||||||
(item) => item.price === "stripe_pro_consumable_empty",
|
|
||||||
);
|
|
||||||
expect(consumableMetered).toBeDefined();
|
expect(consumableMetered).toBeDefined();
|
||||||
expect(consumableEmpty).toBeDefined();
|
|
||||||
expect(consumableEmpty?.quantity).toBe(0);
|
|
||||||
// Metered should not have quantity
|
// Metered should not have quantity
|
||||||
expect("quantity" in (consumableMetered ?? {})).toBe(false);
|
expect("quantity" in (consumableMetered ?? {})).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -588,7 +590,7 @@ describe(
|
|||||||
subscriptionIds: ["sub_123"],
|
subscriptionIds: ["sub_123"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Current subscription has both customer and entity
|
// Current subscription has customer and entity (both use regular metered price - withEntity is false)
|
||||||
const stripeSubscription = stripeSubscriptions.create({
|
const stripeSubscription = stripeSubscriptions.create({
|
||||||
id: "sub_123",
|
id: "sub_123",
|
||||||
items: [
|
items: [
|
||||||
@@ -598,16 +600,6 @@ describe(
|
|||||||
priceId: "stripe_pro_prepaid",
|
priceId: "stripe_pro_prepaid",
|
||||||
quantity: 200,
|
quantity: 200,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "si_pro_consumable",
|
|
||||||
priceId: "stripe_pro_consumable",
|
|
||||||
quantity: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "si_pro_consumable_empty",
|
|
||||||
priceId: "stripe_pro_consumable_empty",
|
|
||||||
quantity: 0,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "si_pro_allocated",
|
id: "si_pro_allocated",
|
||||||
priceId: "stripe_pro_allocated",
|
priceId: "stripe_pro_allocated",
|
||||||
@@ -628,7 +620,7 @@ describe(
|
|||||||
finalCustomerProducts: [entityProProduct],
|
finalCustomerProducts: [entityProProduct],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should update quantities and remove metered consumable
|
// Should update quantities + add metered consumable (wasn't in subscription)
|
||||||
// Fixed: 2 -> 1
|
// Fixed: 2 -> 1
|
||||||
const fixedItem = result.find((item) => item.id === "si_pro_fixed");
|
const fixedItem = result.find((item) => item.id === "si_pro_fixed");
|
||||||
expect(fixedItem?.quantity).toBe(1);
|
expect(fixedItem?.quantity).toBe(1);
|
||||||
@@ -643,11 +635,11 @@ describe(
|
|||||||
);
|
);
|
||||||
expect(allocatedItem?.quantity).toBe(5);
|
expect(allocatedItem?.quantity).toBe(5);
|
||||||
|
|
||||||
// Metered consumable should be deleted
|
// Metered consumable should be added (wasn't in subscription)
|
||||||
const deletedMetered = result.find(
|
const consumableItem = result.find(
|
||||||
(item) => item.id === "si_pro_consumable" && item.deleted,
|
(item) => item.price === "stripe_pro_consumable",
|
||||||
);
|
);
|
||||||
expect(deletedMetered).toBeDefined();
|
expect(consumableItem).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ describe(
|
|||||||
expect(result[0].price).toBe("stripe_premium_consumable");
|
expect(result[0].price).toBe("stripe_premium_consumable");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Entity product, same quantities returns empty array", () => {
|
test("Entity product, same quantities returns only metered addition", () => {
|
||||||
const premium = createProductWithAllPriceTypes({
|
const premium = createProductWithAllPriceTypes({
|
||||||
productId: "premium",
|
productId: "premium",
|
||||||
productName: "Premium",
|
productName: "Premium",
|
||||||
@@ -508,7 +508,8 @@ describe(
|
|||||||
entityId: "entity_1",
|
entityId: "entity_1",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Entity-level includes the empty price with quantity 0
|
// Entity-level subscription has 3 items (no metered consumable)
|
||||||
|
// Note: withEntity is hardcoded to false, so entity products use regular metered prices
|
||||||
const stripeSubscription = stripeSubscriptions.create({
|
const stripeSubscription = stripeSubscriptions.create({
|
||||||
id: "sub_123",
|
id: "sub_123",
|
||||||
items: createStripeItemsFromProduct(premium, { isEntityLevel: true }),
|
items: createStripeItemsFromProduct(premium, { isEntityLevel: true }),
|
||||||
@@ -526,8 +527,9 @@ describe(
|
|||||||
finalCustomerProducts: [entityCustomerProduct],
|
finalCustomerProducts: [entityCustomerProduct],
|
||||||
});
|
});
|
||||||
|
|
||||||
// No changes needed - all quantities match
|
// Only the metered consumable needs to be added (wasn't in existing subscription)
|
||||||
expect(result).toHaveLength(0);
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].price).toBe("stripe_premium_consumable");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -568,14 +570,14 @@ describe(
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Remove entity product marks all items including consumable as deleted", () => {
|
test("Remove entity product marks all items as deleted", () => {
|
||||||
const premium = createProductWithAllPriceTypes({
|
const premium = createProductWithAllPriceTypes({
|
||||||
productId: "premium",
|
productId: "premium",
|
||||||
productName: "Premium",
|
productName: "Premium",
|
||||||
customerProductId: "cus_prod_premium_entity",
|
customerProductId: "cus_prod_premium_entity",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Entity-level subscription has 4 items (including empty consumable)
|
// Entity-level subscription has 3 items (no metered consumable - withEntity is hardcoded to false)
|
||||||
const stripeSubscription = stripeSubscriptions.create({
|
const stripeSubscription = stripeSubscriptions.create({
|
||||||
id: "sub_123",
|
id: "sub_123",
|
||||||
items: createStripeItemsFromProduct(premium, { isEntityLevel: true }),
|
items: createStripeItemsFromProduct(premium, { isEntityLevel: true }),
|
||||||
@@ -593,12 +595,11 @@ describe(
|
|||||||
finalCustomerProducts: [],
|
finalCustomerProducts: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should delete all 4 items
|
// Should delete all 3 items (fixed, prepaid, allocated)
|
||||||
expect(result).toHaveLength(4);
|
expect(result).toHaveLength(3);
|
||||||
expectSubscriptionItemsUpdate(result, [
|
expectSubscriptionItemsUpdate(result, [
|
||||||
{ id: "si_premium_fixed", deleted: true },
|
{ id: "si_premium_fixed", deleted: true },
|
||||||
{ id: "si_premium_prepaid", deleted: true },
|
{ id: "si_premium_prepaid", deleted: true },
|
||||||
{ id: "si_premium_consumable", deleted: true },
|
|
||||||
{ id: "si_premium_allocated", deleted: true },
|
{ id: "si_premium_allocated", deleted: true },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -753,7 +754,7 @@ describe(
|
|||||||
expect(result).toHaveLength(0);
|
expect(result).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Entity-level consumable uses stripe_empty_price_id with quantity 0", () => {
|
test("Entity-level consumable uses regular metered price (withEntity hardcoded to false)", () => {
|
||||||
const premium = createProductWithAllPriceTypes({
|
const premium = createProductWithAllPriceTypes({
|
||||||
productId: "premium",
|
productId: "premium",
|
||||||
productName: "Premium",
|
productName: "Premium",
|
||||||
@@ -787,13 +788,14 @@ describe(
|
|||||||
finalCustomerProducts: [entityCustomerProduct],
|
finalCustomerProducts: [entityCustomerProduct],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find the consumable item
|
// Find the consumable item - now uses regular metered price (withEntity is hardcoded to false)
|
||||||
const consumableItem = result.find((item) =>
|
const consumableItem = result.find((item) =>
|
||||||
item.price?.includes("consumable"),
|
item.price?.includes("consumable"),
|
||||||
);
|
);
|
||||||
expect(consumableItem).toBeDefined();
|
expect(consumableItem).toBeDefined();
|
||||||
expect(consumableItem?.price).toBe("stripe_premium_consumable_empty");
|
expect(consumableItem?.price).toBe("stripe_premium_consumable");
|
||||||
expect(consumableItem?.quantity).toBe(0);
|
// Metered prices should NOT have quantity
|
||||||
|
expect("quantity" in (consumableItem ?? {})).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Customer-level consumable (metered) has no quantity", () => {
|
test("Customer-level consumable (metered) has no quantity", () => {
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { UpdateSubscriptionV0Params } from "@autumn/shared";
|
import type { UpdateSubscriptionV1Params } from "@autumn/shared";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import {
|
import {
|
||||||
computeUpdateSubscriptionIntent,
|
computeUpdateSubscriptionIntent,
|
||||||
UpdateSubscriptionIntent,
|
UpdateSubscriptionIntent,
|
||||||
} from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionIntent";
|
} from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionIntent";
|
||||||
|
|
||||||
const baseParams: UpdateSubscriptionV0Params = {
|
const baseParams: UpdateSubscriptionV1Params = {
|
||||||
customer_id: "cus_test",
|
customer_id: "cus_test",
|
||||||
product_id: "prod_test",
|
product_id: "prod_test",
|
||||||
};
|
};
|
||||||
@@ -23,7 +23,7 @@ const baseParams: UpdateSubscriptionV0Params = {
|
|||||||
describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
||||||
describe(chalk.cyan("Version parameter priority"), () => {
|
describe(chalk.cyan("Version parameter priority"), () => {
|
||||||
test("returns UpdatePlan when version is specified", () => {
|
test("returns UpdatePlan when version is specified", () => {
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
version: 2,
|
version: 2,
|
||||||
};
|
};
|
||||||
@@ -34,7 +34,7 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns UpdatePlan when version is specified even with options", () => {
|
test("returns UpdatePlan when version is specified even with options", () => {
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
version: 3,
|
version: 3,
|
||||||
options: [{ feature_id: "seats", quantity: 10 }],
|
options: [{ feature_id: "seats", quantity: 10 }],
|
||||||
@@ -46,7 +46,7 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns UpdatePlan when version is 0", () => {
|
test("returns UpdatePlan when version is 0", () => {
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
version: 0,
|
version: 0,
|
||||||
};
|
};
|
||||||
@@ -58,8 +58,8 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe(chalk.cyan("UpdateQuantity intent"), () => {
|
describe(chalk.cyan("UpdateQuantity intent"), () => {
|
||||||
test("returns UpdateQuantity when options provided without items", () => {
|
test("returns UpdateQuantity when options provided without customize", () => {
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
options: [{ feature_id: "seats", quantity: 5 }],
|
options: [{ feature_id: "seats", quantity: 5 }],
|
||||||
};
|
};
|
||||||
@@ -70,7 +70,7 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns UpdateQuantity with multiple options", () => {
|
test("returns UpdateQuantity with multiple options", () => {
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
options: [
|
options: [
|
||||||
{ feature_id: "seats", quantity: 5 },
|
{ feature_id: "seats", quantity: 5 },
|
||||||
@@ -85,10 +85,12 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe(chalk.cyan("UpdatePlan intent (default)"), () => {
|
describe(chalk.cyan("UpdatePlan intent (default)"), () => {
|
||||||
test("returns UpdatePlan when items provided", () => {
|
test("returns UpdatePlan when customize provided", () => {
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
items: [{ feature_id: "seats", included_usage: 10 }],
|
customize: {
|
||||||
|
items: [{ feature_id: "seats", included: 10 }],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = computeUpdateSubscriptionIntent(params);
|
const result = computeUpdateSubscriptionIntent(params);
|
||||||
@@ -96,11 +98,13 @@ describe(chalk.yellowBright("computeUpdateSubscriptionIntent"), () => {
|
|||||||
expect(result).toBe(UpdateSubscriptionIntent.UpdatePlan);
|
expect(result).toBe(UpdateSubscriptionIntent.UpdatePlan);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns UpdatePlan when both options and items provided", () => {
|
test("returns UpdatePlan when both options and customize provided", () => {
|
||||||
const params: UpdateSubscriptionV0Params = {
|
const params: UpdateSubscriptionV1Params = {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
options: [{ feature_id: "seats", quantity: 5 }],
|
options: [{ feature_id: "seats", quantity: 5 }],
|
||||||
items: [{ feature_id: "seats", included_usage: 10 }],
|
customize: {
|
||||||
|
items: [{ feature_id: "seats", included: 10 }],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = computeUpdateSubscriptionIntent(params);
|
const result = computeUpdateSubscriptionIntent(params);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const createBilling = ({
|
|||||||
currentEpochMs = Date.now(),
|
currentEpochMs = Date.now(),
|
||||||
billingCycleAnchorMs = "now",
|
billingCycleAnchorMs = "now",
|
||||||
resetCycleAnchorMs = "now",
|
resetCycleAnchorMs = "now",
|
||||||
|
billingVersion = BillingVersion.V1,
|
||||||
}: {
|
}: {
|
||||||
customerProducts?: FullCusProduct[];
|
customerProducts?: FullCusProduct[];
|
||||||
fullProducts?: FullProduct[];
|
fullProducts?: FullProduct[];
|
||||||
@@ -62,6 +63,7 @@ const createBilling = ({
|
|||||||
currentEpochMs?: number;
|
currentEpochMs?: number;
|
||||||
billingCycleAnchorMs?: number | "now";
|
billingCycleAnchorMs?: number | "now";
|
||||||
resetCycleAnchorMs?: number | "now";
|
resetCycleAnchorMs?: number | "now";
|
||||||
|
billingVersion?: BillingVersion;
|
||||||
}): BillingContext => ({
|
}): BillingContext => ({
|
||||||
fullCustomer: customers.create({ customerProducts }),
|
fullCustomer: customers.create({ customerProducts }),
|
||||||
stripeCustomer: stripeCustomers.create(),
|
stripeCustomer: stripeCustomers.create(),
|
||||||
@@ -75,7 +77,7 @@ const createBilling = ({
|
|||||||
customPrices: [],
|
customPrices: [],
|
||||||
customEnts: [],
|
customEnts: [],
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
billingVersion: BillingVersion.V2,
|
billingVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
type EntityBalance,
|
type EntityBalance,
|
||||||
FeatureType,
|
FeatureType,
|
||||||
type FullCustomerEntitlement,
|
type FullCustomerEntitlement,
|
||||||
|
type RolloverConfig,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { entitlements } from "./entitlements";
|
import { entitlements } from "./entitlements";
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ const create = ({
|
|||||||
nextResetAt = null,
|
nextResetAt = null,
|
||||||
entities = null,
|
entities = null,
|
||||||
entityFeatureId = null,
|
entityFeatureId = null,
|
||||||
|
rollover = null,
|
||||||
}: {
|
}: {
|
||||||
id?: string;
|
id?: string;
|
||||||
entitlementId?: string;
|
entitlementId?: string;
|
||||||
@@ -43,6 +45,7 @@ const create = ({
|
|||||||
nextResetAt?: number | null;
|
nextResetAt?: number | null;
|
||||||
entities?: Record<string, EntityBalance> | null;
|
entities?: Record<string, EntityBalance> | null;
|
||||||
entityFeatureId?: string | null;
|
entityFeatureId?: string | null;
|
||||||
|
rollover?: RolloverConfig | null;
|
||||||
}): FullCustomerEntitlement => {
|
}): FullCustomerEntitlement => {
|
||||||
const entId = entitlementId ?? `ent_${featureId}`;
|
const entId = entitlementId ?? `ent_${featureId}`;
|
||||||
return {
|
return {
|
||||||
@@ -72,6 +75,7 @@ const create = ({
|
|||||||
interval,
|
interval,
|
||||||
intervalCount,
|
intervalCount,
|
||||||
entityFeatureId,
|
entityFeatureId,
|
||||||
|
rollover,
|
||||||
}),
|
}),
|
||||||
replaceables: [],
|
replaceables: [],
|
||||||
rollovers: [],
|
rollovers: [],
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { AllowanceType, type EntInterval, FeatureType } from "@autumn/shared";
|
import {
|
||||||
|
AllowanceType,
|
||||||
|
type EntInterval,
|
||||||
|
FeatureType,
|
||||||
|
type RolloverConfig,
|
||||||
|
} from "@autumn/shared";
|
||||||
import { features } from "./features";
|
import { features } from "./features";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,6 +20,7 @@ const create = ({
|
|||||||
interval = null,
|
interval = null,
|
||||||
intervalCount = 1,
|
intervalCount = 1,
|
||||||
entityFeatureId = null,
|
entityFeatureId = null,
|
||||||
|
rollover = null,
|
||||||
}: {
|
}: {
|
||||||
id?: string;
|
id?: string;
|
||||||
featureId: string;
|
featureId: string;
|
||||||
@@ -26,6 +32,7 @@ const create = ({
|
|||||||
interval?: EntInterval | null;
|
interval?: EntInterval | null;
|
||||||
intervalCount?: number;
|
intervalCount?: number;
|
||||||
entityFeatureId?: string | null;
|
entityFeatureId?: string | null;
|
||||||
|
rollover?: RolloverConfig | null;
|
||||||
}) => ({
|
}) => ({
|
||||||
id: id ?? `ent_${featureId}_${crypto.randomUUID().slice(0, 8)}`,
|
id: id ?? `ent_${featureId}_${crypto.randomUUID().slice(0, 8)}`,
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
@@ -40,7 +47,7 @@ const create = ({
|
|||||||
entity_feature_id: entityFeatureId,
|
entity_feature_id: entityFeatureId,
|
||||||
feature_id: featureId,
|
feature_id: featureId,
|
||||||
usage_limit: null,
|
usage_limit: null,
|
||||||
rollover: null,
|
rollover,
|
||||||
feature: features.create({
|
feature: features.create({
|
||||||
id: featureId,
|
id: featureId,
|
||||||
internalId: internalFeatureId,
|
internalId: internalFeatureId,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const createPrepaid = ({
|
|||||||
usage_tiers: [{ to: Infinite, amount: 10 }],
|
usage_tiers: [{ to: Infinite, amount: 10 }],
|
||||||
interval: BillingInterval.Month,
|
interval: BillingInterval.Month,
|
||||||
stripe_price_id: stripePriceId ?? `stripe_price_${id}`,
|
stripe_price_id: stripePriceId ?? `stripe_price_${id}`,
|
||||||
|
stripe_prepaid_price_v2_id: stripePriceId ?? `stripe_price_${id}`,
|
||||||
},
|
},
|
||||||
}) as Price;
|
}) as Price;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { AppEnv, type FullProduct, type Price } from "@autumn/shared";
|
import {
|
||||||
|
AppEnv,
|
||||||
|
type EntitlementWithFeature,
|
||||||
|
type FullProduct,
|
||||||
|
type Price,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a product fixture
|
* Create a product fixture
|
||||||
@@ -33,12 +38,14 @@ const createFull = ({
|
|||||||
id = "prod_test",
|
id = "prod_test",
|
||||||
name = "Test Product",
|
name = "Test Product",
|
||||||
prices = [],
|
prices = [],
|
||||||
|
entitlements = [],
|
||||||
stripeProductId,
|
stripeProductId,
|
||||||
isAddOn = false,
|
isAddOn = false,
|
||||||
}: {
|
}: {
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
prices?: Price[];
|
prices?: Price[];
|
||||||
|
entitlements?: EntitlementWithFeature[];
|
||||||
stripeProductId?: string;
|
stripeProductId?: string;
|
||||||
isAddOn?: boolean;
|
isAddOn?: boolean;
|
||||||
}): FullProduct =>
|
}): FullProduct =>
|
||||||
@@ -58,7 +65,7 @@ const createFull = ({
|
|||||||
base_variant_id: null,
|
base_variant_id: null,
|
||||||
archived: false,
|
archived: false,
|
||||||
prices,
|
prices,
|
||||||
entitlements: [],
|
entitlements,
|
||||||
free_trial: null,
|
free_trial: null,
|
||||||
}) as FullProduct;
|
}) as FullProduct;
|
||||||
|
|
||||||
|
|||||||
133
server/tests/utils/fixtures/itemsV2.ts
Normal file
133
server/tests/utils/fixtures/itemsV2.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import {
|
||||||
|
BillingInterval,
|
||||||
|
BillingMethod,
|
||||||
|
OnDecrease,
|
||||||
|
OnIncrease,
|
||||||
|
ResetInterval,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { TestFeature } from "@tests/setup/v2Features";
|
||||||
|
|
||||||
|
const monthlyPrice = ({
|
||||||
|
amount = 20,
|
||||||
|
intervalCount,
|
||||||
|
}: {
|
||||||
|
amount?: number;
|
||||||
|
intervalCount?: number;
|
||||||
|
} = {}) => ({
|
||||||
|
amount,
|
||||||
|
interval: BillingInterval.Month,
|
||||||
|
interval_count: intervalCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const annualPrice = ({
|
||||||
|
amount = 200,
|
||||||
|
intervalCount,
|
||||||
|
}: {
|
||||||
|
amount?: number;
|
||||||
|
intervalCount?: number;
|
||||||
|
} = {}) => ({
|
||||||
|
amount,
|
||||||
|
interval: BillingInterval.Year,
|
||||||
|
interval_count: intervalCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const monthlyMessages = ({ included = 100 }: { included?: number } = {}) => ({
|
||||||
|
feature_id: TestFeature.Messages,
|
||||||
|
included,
|
||||||
|
reset: {
|
||||||
|
interval: ResetInterval.Month,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const monthlyWords = ({ included = 100 }: { included?: number } = {}) => ({
|
||||||
|
feature_id: TestFeature.Words,
|
||||||
|
included,
|
||||||
|
reset: {
|
||||||
|
interval: ResetInterval.Month,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dashboard = () => ({
|
||||||
|
feature_id: TestFeature.Dashboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prepaidMessages = ({
|
||||||
|
amount = 10,
|
||||||
|
billingUnits = 100,
|
||||||
|
included = 0,
|
||||||
|
}: {
|
||||||
|
amount?: number;
|
||||||
|
billingUnits?: number;
|
||||||
|
included?: number;
|
||||||
|
} = {}) => ({
|
||||||
|
feature_id: TestFeature.Messages,
|
||||||
|
included,
|
||||||
|
price: {
|
||||||
|
amount,
|
||||||
|
interval: BillingInterval.Month,
|
||||||
|
billing_method: BillingMethod.Prepaid,
|
||||||
|
billing_units: billingUnits,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prepaidWords = ({
|
||||||
|
amount = 10,
|
||||||
|
billingUnits = 100,
|
||||||
|
included = 0,
|
||||||
|
}: {
|
||||||
|
amount?: number;
|
||||||
|
billingUnits?: number;
|
||||||
|
included?: number;
|
||||||
|
} = {}) => ({
|
||||||
|
feature_id: TestFeature.Words,
|
||||||
|
included,
|
||||||
|
price: {
|
||||||
|
amount,
|
||||||
|
interval: BillingInterval.Month,
|
||||||
|
billing_method: BillingMethod.Prepaid,
|
||||||
|
billing_units: billingUnits,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const consumableMessages = ({ amount = 1 }: { amount?: number } = {}) => ({
|
||||||
|
feature_id: TestFeature.Messages,
|
||||||
|
price: {
|
||||||
|
amount,
|
||||||
|
interval: BillingInterval.Month,
|
||||||
|
billing_method: BillingMethod.UsageBased,
|
||||||
|
billing_units: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const allocatedUsers = ({
|
||||||
|
amount = 10,
|
||||||
|
included = 0,
|
||||||
|
}: {
|
||||||
|
amount?: number;
|
||||||
|
included?: number;
|
||||||
|
} = {}) => ({
|
||||||
|
feature_id: TestFeature.Users,
|
||||||
|
included,
|
||||||
|
price: {
|
||||||
|
amount,
|
||||||
|
interval: BillingInterval.Month,
|
||||||
|
billing_method: BillingMethod.UsageBased,
|
||||||
|
billing_units: 1,
|
||||||
|
},
|
||||||
|
proration: {
|
||||||
|
on_increase: OnIncrease.ProrateImmediately,
|
||||||
|
on_decrease: OnDecrease.Prorate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const itemsV2 = {
|
||||||
|
monthlyPrice,
|
||||||
|
annualPrice,
|
||||||
|
monthlyMessages,
|
||||||
|
monthlyWords,
|
||||||
|
dashboard,
|
||||||
|
prepaidMessages,
|
||||||
|
prepaidWords,
|
||||||
|
consumableMessages,
|
||||||
|
allocatedUsers,
|
||||||
|
} as const;
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
defineVersionChange,
|
defineVersionChange,
|
||||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
import type { SharedContext } from "../../../../types/sharedContext.js";
|
||||||
import { CheckQuerySchema } from "../checkParams.js";
|
import { CheckQuerySchema } from "../checkParams.js";
|
||||||
import { CheckExpand } from "../enums/CheckExpand.js";
|
import { CheckExpand } from "../enums/CheckExpand.js";
|
||||||
|
|
||||||
@@ -42,8 +43,10 @@ export const V1_2_CheckQueryChange = defineVersionChange({
|
|||||||
|
|
||||||
// Request: V1.2 → V2.0 (add expand option)
|
// Request: V1.2 → V2.0 (add expand option)
|
||||||
transformRequest: ({
|
transformRequest: ({
|
||||||
|
ctx: _ctx,
|
||||||
input,
|
input,
|
||||||
}: {
|
}: {
|
||||||
|
ctx: SharedContext;
|
||||||
input: z.infer<typeof CheckQuerySchema>;
|
input: z.infer<typeof CheckQuerySchema>;
|
||||||
}) => {
|
}) => {
|
||||||
const existingExpand = input.expand || [];
|
const existingExpand = input.expand || [];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
defineVersionChange,
|
defineVersionChange,
|
||||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
import type { SharedContext } from "../../../../types/sharedContext.js";
|
||||||
import { TrackParamsSchema } from "../trackParams.js";
|
import { TrackParamsSchema } from "../trackParams.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,8 +32,10 @@ export const V1_2_TrackParamsChange = defineVersionChange({
|
|||||||
|
|
||||||
// Request: V1.2 → V2.0 (extract properties.value to value if not already set)
|
// Request: V1.2 → V2.0 (extract properties.value to value if not already set)
|
||||||
transformRequest: ({
|
transformRequest: ({
|
||||||
|
ctx: _ctx,
|
||||||
input,
|
input,
|
||||||
}: {
|
}: {
|
||||||
|
ctx: SharedContext;
|
||||||
input: z.infer<typeof TrackParamsSchema>;
|
input: z.infer<typeof TrackParamsSchema>;
|
||||||
}): z.infer<typeof TrackParamsSchema> => {
|
}): z.infer<typeof TrackParamsSchema> => {
|
||||||
// Keep original value if provided, otherwise extract from properties.value
|
// Keep original value if provided, otherwise extract from properties.value
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
import { RedirectModeSchema } from "@api/billing/common/redirectMode.js";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { PlanTimingSchema } from "../../../models/billingModels/context/attachBillingContext.js";
|
import { PlanTimingSchema } from "../../../models/billingModels/context/attachBillingContext.js";
|
||||||
import { ProductItemSchema } from "../../../models/productV2Models/productItemModels/productItemModels.js";
|
import { ProductItemSchema } from "../../../models/productV2Models/productItemModels/productItemModels.js";
|
||||||
import { BillingBehaviorSchema } from "../common/billingBehavior.js";
|
import { BillingBehaviorSchema } from "../common/billingBehavior.js";
|
||||||
import { BillingParamsBaseSchema } from "../common/billingParamsBase.js";
|
import { BillingParamsBaseV0Schema } from "../common/billingParamsBase/billingParamsBaseV0.js";
|
||||||
|
|
||||||
export const RedirectModeSchema = z.enum(["always", "if_required", "never"]);
|
export const ExtAttachParamsV0Schema = BillingParamsBaseV0Schema.extend({
|
||||||
export type RedirectMode = z.infer<typeof RedirectModeSchema>;
|
|
||||||
|
|
||||||
export const ExtAttachParamsV0Schema = BillingParamsBaseSchema.extend({
|
|
||||||
// Product identification
|
// Product identification
|
||||||
product_id: z.string(),
|
product_id: z.string(),
|
||||||
|
|
||||||
|
|||||||
48
shared/api/billing/attachV2/attachParamsV1.ts
Normal file
48
shared/api/billing/attachV2/attachParamsV1.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { BillingParamsBaseV1Schema } from "@api/billing/common/billingParamsBase/billingParamsBaseV1.js";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { PlanTimingSchema } from "../../../models/billingModels/context/attachBillingContext.js";
|
||||||
|
import { BillingBehaviorSchema } from "../common/billingBehavior.js";
|
||||||
|
import { RedirectModeSchema } from "../common/redirectMode.js";
|
||||||
|
|
||||||
|
export const AttachParamsV1Schema = BillingParamsBaseV1Schema.extend({
|
||||||
|
// Product identification
|
||||||
|
product_id: z.string(),
|
||||||
|
|
||||||
|
// Invoice mode
|
||||||
|
invoice: z.boolean().optional(),
|
||||||
|
enable_product_immediately: z.boolean().optional(),
|
||||||
|
finalize_invoice: z.boolean().optional(),
|
||||||
|
// invoice_mode: z
|
||||||
|
// .object({
|
||||||
|
// enabled: z.boolean(),
|
||||||
|
// enable_product_immediately: z.boolean(),
|
||||||
|
// finalize_invoice: z.boolean(),
|
||||||
|
// })
|
||||||
|
// .optional(),
|
||||||
|
|
||||||
|
// Checkout behavior
|
||||||
|
redirect_mode: RedirectModeSchema.default("always"),
|
||||||
|
success_url: z.string().optional(),
|
||||||
|
new_billing_subscription: z.boolean().optional(),
|
||||||
|
plan_schedule: PlanTimingSchema.optional(),
|
||||||
|
billing_behavior: BillingBehaviorSchema.optional(),
|
||||||
|
adjustable_quantity: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// export const AttachParamsV1Schema = ExtAttachParamsV1Schema.extend({
|
||||||
|
// // Custom product configuration
|
||||||
|
// items: z.array(ProductItemSchema).optional(),
|
||||||
|
// }).refine(
|
||||||
|
// (data) => {
|
||||||
|
// if (data.items && data.items.length === 0) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// return true;
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// message: "Must provide at least one item when using custom plan",
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
export type AttachParamsV1 = z.infer<typeof AttachParamsV1Schema>;
|
||||||
|
export type AttachParamsV1Input = z.input<typeof AttachParamsV1Schema>;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { freeTrialParamsV0ToV1 } from "@api/common/freeTrial/mappers/freeTrialParamsV0ToV1.js";
|
||||||
|
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||||
|
import {
|
||||||
|
AffectedResource,
|
||||||
|
defineVersionChange,
|
||||||
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
|
import { productItemsToCustomizePlanV1 } from "@utils/productV2Utils/productItemUtils/convertProductItem/productItemsToCustomizePlanV1.js";
|
||||||
|
import type { z } from "zod/v4";
|
||||||
|
import type { SharedContext } from "../../../../types/sharedContext.js";
|
||||||
|
import { AttachParamsV0Schema } from "../attachParamsV0.js";
|
||||||
|
import { AttachParamsV1Schema } from "../attachParamsV1.js";
|
||||||
|
|
||||||
|
export const V1_2_AttachParamsChange = defineVersionChange({
|
||||||
|
name: "V1.2 Attach Params Change",
|
||||||
|
newVersion: ApiVersion.V2_0,
|
||||||
|
oldVersion: ApiVersion.V1_Beta,
|
||||||
|
description: [
|
||||||
|
"Maps free_trial {length,duration} to {duration_length,duration_type}",
|
||||||
|
"Maps top-level items to customize.items",
|
||||||
|
],
|
||||||
|
affectedResources: [AffectedResource.Attach],
|
||||||
|
newSchema: AttachParamsV1Schema,
|
||||||
|
oldSchema: AttachParamsV0Schema,
|
||||||
|
affectsRequest: true,
|
||||||
|
affectsResponse: false,
|
||||||
|
transformRequest: ({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
ctx: SharedContext;
|
||||||
|
input: z.infer<typeof AttachParamsV0Schema>;
|
||||||
|
}): z.infer<typeof AttachParamsV1Schema> => {
|
||||||
|
const customizeV1 = input.items
|
||||||
|
? productItemsToCustomizePlanV1({
|
||||||
|
ctx,
|
||||||
|
items: input.items,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const freeTrialV1 = freeTrialParamsV0ToV1({
|
||||||
|
freeTrialParamsV0: input.free_trial,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...input,
|
||||||
|
free_trial: freeTrialV1,
|
||||||
|
customize: customizeV1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -2,10 +2,10 @@ import { FeatureOptionsParamsV0Schema } from "@api/billing/common/featureOptions
|
|||||||
import { FreeTrialParamsV0Schema } from "@api/common/freeTrial/freeTrialParamsV0.js";
|
import { FreeTrialParamsV0Schema } from "@api/common/freeTrial/freeTrialParamsV0.js";
|
||||||
import { ProductItemSchema } from "@models/productV2Models/productItemModels/productItemModels.js";
|
import { ProductItemSchema } from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { CustomerDataSchema } from "../../common/customerData.js";
|
import { CustomerDataSchema } from "../../../common/customerData.js";
|
||||||
import { EntityDataSchema } from "../../common/entityData.js";
|
import { EntityDataSchema } from "../../../common/entityData.js";
|
||||||
|
|
||||||
export const BillingParamsBaseSchema = z.object({
|
export const BillingParamsBaseV0Schema = z.object({
|
||||||
customer_id: z.string(),
|
customer_id: z.string(),
|
||||||
entity_id: z.string().nullish(),
|
entity_id: z.string().nullish(),
|
||||||
customer_data: CustomerDataSchema.optional(),
|
customer_data: CustomerDataSchema.optional(),
|
||||||
@@ -18,4 +18,4 @@ export const BillingParamsBaseSchema = z.object({
|
|||||||
items: z.array(ProductItemSchema).optional(),
|
items: z.array(ProductItemSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BillingParamsBase = z.infer<typeof BillingParamsBaseSchema>;
|
export type BillingParamsBaseV0 = z.infer<typeof BillingParamsBaseV0Schema>;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { FeatureOptionsParamsV0Schema } from "@api/billing/common/featureOptions/featureOptionsParamsV0.js";
|
||||||
|
import { FreeTrialParamsV1Schema } from "@api/common/freeTrial/freeTrialParamsV1.js";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { CustomerDataSchema } from "../../../common/customerData.js";
|
||||||
|
import { EntityDataSchema } from "../../../common/entityData.js";
|
||||||
|
import { CustomizePlanV1Schema } from "../customizePlan/customizePlanV1.js";
|
||||||
|
|
||||||
|
export const BillingParamsBaseV1Schema = z.object({
|
||||||
|
customer_id: z.string(),
|
||||||
|
entity_id: z.string().nullish(),
|
||||||
|
customer_data: CustomerDataSchema.optional(),
|
||||||
|
entity_data: EntityDataSchema.optional(),
|
||||||
|
|
||||||
|
// Used for both update and attach
|
||||||
|
options: z.array(FeatureOptionsParamsV0Schema).nullish(),
|
||||||
|
version: z.number().optional(),
|
||||||
|
|
||||||
|
free_trial: FreeTrialParamsV1Schema.nullable().optional(),
|
||||||
|
customize: CustomizePlanV1Schema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BillingParamsBaseV1 = z.infer<typeof BillingParamsBaseV1Schema>;
|
||||||
@@ -28,7 +28,6 @@ export const BillingResponseSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
payment_url: z.string().nullable(),
|
payment_url: z.string().nullable(),
|
||||||
|
|
||||||
required_action: BillingResponseRequiredActionSchema.optional(),
|
required_action: BillingResponseRequiredActionSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ProductItemSchema } from "@models/productV2Models/productItemModels/productItemModels";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const CustomizePlanV0Schema = z.array(ProductItemSchema);
|
||||||
|
|
||||||
|
export type CustomizePlanV0 = z.infer<typeof CustomizePlanV0Schema>;
|
||||||
14
shared/api/billing/common/customizePlan/customizePlanV1.ts
Normal file
14
shared/api/billing/common/customizePlan/customizePlanV1.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { BasePriceParamsSchema } from "@api/products/components/basePrice/basePrice";
|
||||||
|
import { CreatePlanItemParamsV1Schema } from "@api/products/items/crud/createPlanItemParamsV1";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const CustomizePlanV1Schema = z
|
||||||
|
.object({
|
||||||
|
price: BasePriceParamsSchema.nullable().optional(), // null to remove base price
|
||||||
|
items: z.array(CreatePlanItemParamsV1Schema).optional(),
|
||||||
|
})
|
||||||
|
.refine((data) => data.items !== undefined || data.price !== undefined, {
|
||||||
|
message: "When using customize, either items or price must be provided",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CustomizePlanV1 = z.infer<typeof CustomizePlanV1Schema>;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { basePriceToProductItem } from "@api/products/components/basePrice/basePriceToProductItem";
|
||||||
|
import { planV1ToProductItems } from "@api/products/mappers/planV1ToProductItems";
|
||||||
|
import type { FullProduct } from "@models/productModels/productModels";
|
||||||
|
import { isPriceItem, mapToProductItems } from "@utils/index";
|
||||||
|
import type { SharedContext } from "../../../../../types/sharedContext";
|
||||||
|
import type { CustomizePlanV0 } from "../customizePlanV0";
|
||||||
|
import type { CustomizePlanV1 } from "../customizePlanV1";
|
||||||
|
|
||||||
|
export const customizePlanV1ToV0 = ({
|
||||||
|
ctx,
|
||||||
|
customizePlanV1,
|
||||||
|
fullProduct,
|
||||||
|
}: {
|
||||||
|
ctx: SharedContext;
|
||||||
|
customizePlanV1: CustomizePlanV1;
|
||||||
|
fullProduct: FullProduct;
|
||||||
|
}): CustomizePlanV0 => {
|
||||||
|
const currentProductItems = mapToProductItems({
|
||||||
|
prices: fullProduct.prices,
|
||||||
|
entitlements: fullProduct.entitlements,
|
||||||
|
features: ctx.features,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
customizePlanV1.price !== undefined &&
|
||||||
|
customizePlanV1.items !== undefined
|
||||||
|
) {
|
||||||
|
// 1. If price AND items provided, return full items array
|
||||||
|
return planV1ToProductItems({
|
||||||
|
ctx,
|
||||||
|
plan: { price: customizePlanV1.price, items: customizePlanV1.items },
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
customizePlanV1.price !== undefined &&
|
||||||
|
customizePlanV1.items === undefined
|
||||||
|
) {
|
||||||
|
// 2. If price provided, but no items, then customize base price and carry over feature items
|
||||||
|
const featureItems = currentProductItems.filter(
|
||||||
|
(item) => !isPriceItem(item),
|
||||||
|
);
|
||||||
|
|
||||||
|
const basePriceItem = customizePlanV1.price
|
||||||
|
? basePriceToProductItem({
|
||||||
|
ctx,
|
||||||
|
basePrice: customizePlanV1.price,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return basePriceItem ? [basePriceItem, ...featureItems] : featureItems;
|
||||||
|
} else {
|
||||||
|
// 3. If no price provided, then carry over base price
|
||||||
|
const basePriceItem = currentProductItems.filter((item) =>
|
||||||
|
isPriceItem(item),
|
||||||
|
);
|
||||||
|
const featureItems = planV1ToProductItems({
|
||||||
|
ctx,
|
||||||
|
plan: { price: null, items: customizePlanV1.items ?? [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...basePriceItem, ...featureItems];
|
||||||
|
}
|
||||||
|
};
|
||||||
4
shared/api/billing/common/redirectMode.ts
Normal file
4
shared/api/billing/common/redirectMode.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const RedirectModeSchema = z.enum(["always", "if_required", "never"]);
|
||||||
|
export type RedirectMode = z.infer<typeof RedirectModeSchema>;
|
||||||
@@ -4,16 +4,24 @@ export * from "./attach/prevVersions/attachBodyV0.js";
|
|||||||
export * from "./attach/prevVersions/attachResponseV1.js";
|
export * from "./attach/prevVersions/attachResponseV1.js";
|
||||||
// Attach V2
|
// Attach V2
|
||||||
export * from "./attachV2/attachParamsV0.js";
|
export * from "./attachV2/attachParamsV0.js";
|
||||||
|
export * from "./attachV2/attachParamsV1.js";
|
||||||
|
|
||||||
// Checkout
|
// Checkout
|
||||||
|
|
||||||
export * from "./checkout/prevVersions/checkoutParamsV0.js";
|
export * from "./checkout/prevVersions/checkoutParamsV0.js";
|
||||||
export * from "./checkout/prevVersions/checkoutResponseV0.js";
|
export * from "./checkout/prevVersions/checkoutResponseV0.js";
|
||||||
// Common
|
// Common
|
||||||
export * from "./common/billingParamsBase.js";
|
export * from "./common/billingParamsBase/billingParamsBaseV0.js";
|
||||||
|
export * from "./common/billingParamsBase/billingParamsBaseV1.js";
|
||||||
export * from "./common/billingPreviewResponse.js";
|
export * from "./common/billingPreviewResponse.js";
|
||||||
export * from "./common/billingResponse.js";
|
export * from "./common/billingResponse.js";
|
||||||
export * from "./common/cancelAction.js";
|
export * from "./common/cancelAction.js";
|
||||||
|
export * from "./common/customizePlan/customizePlanV0.js";
|
||||||
|
export * from "./common/customizePlan/customizePlanV1.js";
|
||||||
|
export * from "./common/customizePlan/mappers/customizePlanV1ToV0.js";
|
||||||
|
export * from "./common/redirectMode.js";
|
||||||
export * from "./common/refundBehavior.js";
|
export * from "./common/refundBehavior.js";
|
||||||
export * from "./updateSubscription/previewUpdateSubscriptionResponse.js";
|
export * from "./updateSubscription/previewUpdateSubscriptionResponse.js";
|
||||||
// Update Subscription
|
// Update Subscription
|
||||||
export * from "./updateSubscription/updateSubscriptionV0Params.js";
|
export * from "./updateSubscription/updateSubscriptionV0Params.js";
|
||||||
|
export * from "./updateSubscription/updateSubscriptionV1Params.js";
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { freeTrialParamsV0ToV1 } from "@api/common/freeTrial/mappers/freeTrialParamsV0ToV1.js";
|
||||||
|
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||||
|
import {
|
||||||
|
AffectedResource,
|
||||||
|
defineVersionChange,
|
||||||
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
|
import { productItemsToCustomizePlanV1 } from "@utils/productV2Utils/productItemUtils/convertProductItem/productItemsToCustomizePlanV1.js";
|
||||||
|
import type { z } from "zod/v4";
|
||||||
|
import type { SharedContext } from "../../../../types/sharedContext.js";
|
||||||
|
import { UpdateSubscriptionV0ParamsSchema } from "../updateSubscriptionV0Params.js";
|
||||||
|
import { UpdateSubscriptionV1ParamsSchema } from "../updateSubscriptionV1Params.js";
|
||||||
|
|
||||||
|
export const V1_2_UpdateSubscriptionParamsChange = defineVersionChange({
|
||||||
|
name: "V1.2 Update Subscription Params Change",
|
||||||
|
newVersion: ApiVersion.V2_0,
|
||||||
|
oldVersion: ApiVersion.V1_Beta,
|
||||||
|
description: [
|
||||||
|
"Maps free_trial {length,duration} to {duration_length,duration_type}",
|
||||||
|
"Maps top-level items to customize.items",
|
||||||
|
],
|
||||||
|
affectedResources: [AffectedResource.ApiSubscriptionUpdate],
|
||||||
|
newSchema: UpdateSubscriptionV1ParamsSchema,
|
||||||
|
oldSchema: UpdateSubscriptionV0ParamsSchema,
|
||||||
|
affectsRequest: true,
|
||||||
|
affectsResponse: false,
|
||||||
|
transformRequest: ({
|
||||||
|
ctx,
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
ctx: SharedContext;
|
||||||
|
input: z.infer<typeof UpdateSubscriptionV0ParamsSchema>;
|
||||||
|
}): z.infer<typeof UpdateSubscriptionV1ParamsSchema> => {
|
||||||
|
const customizeV1 = input.items
|
||||||
|
? productItemsToCustomizePlanV1({
|
||||||
|
ctx,
|
||||||
|
items: input.items,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const freeTrialV1 = freeTrialParamsV0ToV1({
|
||||||
|
freeTrialParamsV0: input.free_trial,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...input,
|
||||||
|
free_trial: freeTrialV1,
|
||||||
|
customize: customizeV1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -2,11 +2,11 @@ import { RefundBehaviorSchema } from "@api/billing/common/refundBehavior";
|
|||||||
import { nullish } from "@utils/utils";
|
import { nullish } from "@utils/utils";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { BillingBehaviorSchema } from "../common/billingBehavior";
|
import { BillingBehaviorSchema } from "../common/billingBehavior";
|
||||||
import { BillingParamsBaseSchema } from "../common/billingParamsBase";
|
import { BillingParamsBaseV0Schema } from "../common/billingParamsBase/billingParamsBaseV0";
|
||||||
import { CancelActionSchema } from "../common/cancelAction";
|
import { CancelActionSchema } from "../common/cancelAction";
|
||||||
|
|
||||||
export const ExtUpdateSubscriptionV0ParamsSchema =
|
export const ExtUpdateSubscriptionV0ParamsSchema =
|
||||||
BillingParamsBaseSchema.extend({
|
BillingParamsBaseV0Schema.extend({
|
||||||
// Product identification (optional for update subscription - can target by customer_product_id)
|
// Product identification (optional for update subscription - can target by customer_product_id)
|
||||||
product_id: z.string().nullish(),
|
product_id: z.string().nullish(),
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { nullish } from "@utils/utils";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { BillingBehaviorSchema } from "../common/billingBehavior";
|
||||||
|
import { BillingParamsBaseV1Schema } from "../common/billingParamsBase/billingParamsBaseV1";
|
||||||
|
import { CancelActionSchema } from "../common/cancelAction";
|
||||||
|
|
||||||
|
export const UpdateSubscriptionV1ParamsSchema =
|
||||||
|
BillingParamsBaseV1Schema.extend({
|
||||||
|
product_id: z.string().nullish(),
|
||||||
|
|
||||||
|
invoice: z.boolean().optional(),
|
||||||
|
enable_product_immediately: z.boolean().optional(),
|
||||||
|
finalize_invoice: z.boolean().optional(),
|
||||||
|
|
||||||
|
cancel_action: CancelActionSchema.optional(),
|
||||||
|
billing_behavior: BillingBehaviorSchema.optional(),
|
||||||
|
|
||||||
|
customer_product_id: z.string().optional().meta({
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
.check((ctx) => {
|
||||||
|
if (ctx.value.options && ctx.value.options.length > 0) {
|
||||||
|
const invalidFeatures = ctx.value.options
|
||||||
|
.filter((opt) => nullish(opt.quantity) || opt.quantity < 0)
|
||||||
|
.map((opt) => opt.feature_id);
|
||||||
|
|
||||||
|
if (invalidFeatures.length > 0) {
|
||||||
|
ctx.issues.push({
|
||||||
|
code: "custom",
|
||||||
|
message: `Options quantity must be >= 0 for features: ${invalidFeatures.join(", ")}`,
|
||||||
|
input: ctx.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.cancel_action !== "cancel_immediately") return true;
|
||||||
|
|
||||||
|
const forbiddenFields = [
|
||||||
|
"options",
|
||||||
|
"version",
|
||||||
|
"free_trial",
|
||||||
|
"customize",
|
||||||
|
] as const;
|
||||||
|
return !forbiddenFields.some((field) => data[field] !== undefined);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Cannot pass options, items, version, or free_trial when cancel_action is 'cancel_immediately'. Immediate cancellation only processes a prorated refund.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.cancel_action !== "cancel_end_of_cycle") return true;
|
||||||
|
|
||||||
|
// Cannot pass free_trial when cancel_action is 'cancel_end_of_cycle'
|
||||||
|
return data.free_trial === undefined;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Cannot pass free_trial when cancel_action is 'cancel_end_of_cycle'.",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type UpdateSubscriptionV1Params = z.infer<
|
||||||
|
typeof UpdateSubscriptionV1ParamsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type UpdateSubscriptionV1ParamsInput = z.input<
|
||||||
|
typeof UpdateSubscriptionV1ParamsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Schedules (epoch milliseconds)
|
||||||
|
// plan_custom_start_date: z.number().optional(),
|
||||||
|
// billing_cycle_anchor: z.number().optional(),
|
||||||
|
|
||||||
|
// keep_existing_plan: true, //disable_plan_switch
|
||||||
|
// prorate_billing: true,
|
||||||
|
// invoice_only: true,
|
||||||
|
|
||||||
|
// carry_over_balance: true,
|
||||||
|
// reset_usage: true,
|
||||||
|
|
||||||
|
// billing_custom_start_date: "2025-11-04",
|
||||||
|
// billing_custom_end_date: "2025-12-04",
|
||||||
|
// billing_cycle_anchor: "2025-11-04",
|
||||||
|
// billing_due_date: "2025-11-04",
|
||||||
|
|
||||||
|
// new_billing_subscription: true, //fka combine_subscriptions, separate_billing_subscriptions
|
||||||
|
// require_payment_method: true, //fka force_checkout
|
||||||
|
// reset_balances: true
|
||||||
|
|
||||||
|
// plan_schedule: "immediate", // or "next_cycle", "custom_date"
|
||||||
|
// plan_custom_start_date: "2025-11-04",
|
||||||
|
// plan_custom_end_date: "2025-12-04",
|
||||||
|
// billing_schedule: "immediate", // or "next_cycle", "custom_date"
|
||||||
20
shared/api/common/freeTrial/mappers/freeTrialParamsV0ToV1.ts
Normal file
20
shared/api/common/freeTrial/mappers/freeTrialParamsV0ToV1.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { FreeTrialParamsV0 } from "@api/common/freeTrial/freeTrialParamsV0";
|
||||||
|
import type { FreeTrialParamsV1 } from "@api/common/freeTrial/freeTrialParamsV1";
|
||||||
|
|
||||||
|
export const freeTrialParamsV0ToV1 = ({
|
||||||
|
freeTrialParamsV0,
|
||||||
|
}: {
|
||||||
|
freeTrialParamsV0: FreeTrialParamsV0 | null | undefined;
|
||||||
|
}): FreeTrialParamsV1 | null | undefined => {
|
||||||
|
// If it's undefined, means no action
|
||||||
|
if (freeTrialParamsV0 === undefined) return undefined;
|
||||||
|
|
||||||
|
// If it's null, means remove the trial
|
||||||
|
if (freeTrialParamsV0 === null) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
duration_length: freeTrialParamsV0.length,
|
||||||
|
duration_type: freeTrialParamsV0.duration,
|
||||||
|
card_required: freeTrialParamsV0.card_required,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
import type { SharedContext } from "../../../types/sharedContext.js";
|
||||||
import { GetCustomerQuerySchema } from "../customerOpModels.js";
|
import { GetCustomerQuerySchema } from "../customerOpModels.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,8 +44,10 @@ export const V1_2_CustomerQueryChange = defineVersionChange({
|
|||||||
|
|
||||||
// Request: V1.2 → V2.0 (add expand options)
|
// Request: V1.2 → V2.0 (add expand options)
|
||||||
transformRequest: ({
|
transformRequest: ({
|
||||||
|
ctx: _ctx,
|
||||||
input,
|
input,
|
||||||
}: {
|
}: {
|
||||||
|
ctx: SharedContext;
|
||||||
input: z.infer<typeof GetCustomerQuerySchema>;
|
input: z.infer<typeof GetCustomerQuerySchema>;
|
||||||
}) => {
|
}) => {
|
||||||
const existingExpand = input.expand || [];
|
const existingExpand = input.expand || [];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
import type { SharedContext } from "../../../types/sharedContext.js";
|
||||||
import {
|
import {
|
||||||
type GetEntityQuery,
|
type GetEntityQuery,
|
||||||
GetEntityQuerySchema,
|
GetEntityQuerySchema,
|
||||||
@@ -46,8 +47,10 @@ export const V1_2_EntityQueryChange = defineVersionChange({
|
|||||||
|
|
||||||
// Request: V1.2 → V2.0 (add expand options)
|
// Request: V1.2 → V2.0 (add expand options)
|
||||||
transformRequest: ({
|
transformRequest: ({
|
||||||
|
ctx: _ctx,
|
||||||
input,
|
input,
|
||||||
}: {
|
}: {
|
||||||
|
ctx: SharedContext;
|
||||||
input: z.infer<typeof GetEntityQuerySchema>;
|
input: z.infer<typeof GetEntityQuerySchema>;
|
||||||
}) => {
|
}) => {
|
||||||
const existingExpand = input.expand || [];
|
const existingExpand = input.expand || [];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
defineVersionChange,
|
defineVersionChange,
|
||||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
import type { SharedContext } from "../../../types/sharedContext.js";
|
||||||
import { featureV0ToV1Type } from "../../../utils/featureUtils/convertFeatureUtils.js";
|
import { featureV0ToV1Type } from "../../../utils/featureUtils/convertFeatureUtils.js";
|
||||||
import { CreateFeatureV1ParamsSchema } from "../featureV1OpModels.js";
|
import { CreateFeatureV1ParamsSchema } from "../featureV1OpModels.js";
|
||||||
import { CreateFeatureV0ParamsSchema } from "../prevVersions/featureV0OpModels.js";
|
import { CreateFeatureV0ParamsSchema } from "../prevVersions/featureV0OpModels.js";
|
||||||
@@ -46,8 +47,10 @@ export const V1_2_CreateFeatureChange = defineVersionChange({
|
|||||||
|
|
||||||
// Request: V0 → V1 (old format to new)
|
// Request: V0 → V1 (old format to new)
|
||||||
transformRequest: ({
|
transformRequest: ({
|
||||||
|
ctx: _ctx,
|
||||||
input,
|
input,
|
||||||
}: {
|
}: {
|
||||||
|
ctx: SharedContext;
|
||||||
input: z.infer<typeof CreateFeatureV0ParamsSchema>;
|
input: z.infer<typeof CreateFeatureV0ParamsSchema>;
|
||||||
}): z.infer<typeof CreateFeatureV1ParamsSchema> => {
|
}): z.infer<typeof CreateFeatureV1ParamsSchema> => {
|
||||||
const { type, consumable } = featureV0ToV1Type({ type: input.type });
|
const { type, consumable } = featureV0ToV1Type({ type: input.type });
|
||||||
|
|||||||
26
shared/api/products/components/basePrice/basePrice.ts
Normal file
26
shared/api/products/components/basePrice/basePrice.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { BillingInterval } from "@models/productModels/intervals/billingInterval";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { DisplaySchema } from "../display";
|
||||||
|
|
||||||
|
export const BasePriceSchema = z.object({
|
||||||
|
amount: z.number(),
|
||||||
|
interval: z.enum(BillingInterval),
|
||||||
|
interval_count: z.number().optional(),
|
||||||
|
display: DisplaySchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BasePriceParamsSchema = BasePriceSchema.omit({
|
||||||
|
display: true,
|
||||||
|
}).extend({
|
||||||
|
interval_count: z.number().optional(),
|
||||||
|
|
||||||
|
entitlement_id: z.string().optional().meta({
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
|
price_id: z.string().optional().meta({
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BasePrice = z.infer<typeof BasePriceSchema>;
|
||||||
|
export type BasePriceParams = z.infer<typeof BasePriceParamsSchema>;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type {
|
||||||
|
BasePrice,
|
||||||
|
BasePriceParams,
|
||||||
|
} from "@api/products/components/basePrice/basePrice";
|
||||||
|
import { BillingInterval } from "@models/productModels/intervals/billingInterval";
|
||||||
|
import {
|
||||||
|
type ProductItem,
|
||||||
|
ProductItemType,
|
||||||
|
} from "@models/productV2Models/productItemModels/productItemModels";
|
||||||
|
import { getProductItemDisplay } from "@utils/productDisplayUtils";
|
||||||
|
import { billingToItemInterval } from "@utils/productV2Utils/productItemUtils/itemIntervalUtils";
|
||||||
|
import type { SharedContext } from "../../../../types/sharedContext";
|
||||||
|
|
||||||
|
export const basePriceToProductItem = ({
|
||||||
|
ctx,
|
||||||
|
basePrice,
|
||||||
|
}: {
|
||||||
|
ctx: SharedContext;
|
||||||
|
basePrice: BasePrice | BasePriceParams;
|
||||||
|
}): ProductItem => {
|
||||||
|
const basePriceDisplay =
|
||||||
|
"display" in basePrice ? basePrice.display : undefined;
|
||||||
|
|
||||||
|
const entitlementId =
|
||||||
|
"entitlement_id" in basePrice
|
||||||
|
? (basePrice.entitlement_id ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
const priceId =
|
||||||
|
"price_id" in basePrice ? (basePrice.price_id ?? undefined) : undefined;
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
type: ProductItemType.Price,
|
||||||
|
feature_id: null,
|
||||||
|
feature: null,
|
||||||
|
interval: billingToItemInterval({
|
||||||
|
billingInterval: basePrice.interval ?? BillingInterval.Month,
|
||||||
|
}),
|
||||||
|
interval_count: basePrice.interval_count ?? 1,
|
||||||
|
price: basePrice.amount ?? 0,
|
||||||
|
|
||||||
|
entitlement_id: entitlementId,
|
||||||
|
price_id: priceId,
|
||||||
|
} satisfies ProductItem;
|
||||||
|
|
||||||
|
const display = basePriceDisplay
|
||||||
|
? getProductItemDisplay({ item, features: ctx.features })
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
display,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FreeTrialParamsV1Schema } from "@api/common/freeTrial/freeTrialParamsV1.js";
|
import { FreeTrialParamsV1Schema } from "@api/common/freeTrial/freeTrialParamsV1.js";
|
||||||
import { BillingInterval } from "@models/productModels/intervals/billingInterval.js";
|
import { BasePriceParamsSchema } from "@api/products/components/basePrice/basePrice.js";
|
||||||
import { idRegex } from "@utils/utils.js";
|
import { idRegex } from "@utils/utils.js";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { CreatePlanItemParamsV1Schema } from "../items/crud/createPlanItemParamsV1.js";
|
import { CreatePlanItemParamsV1Schema } from "../items/crud/createPlanItemParamsV1.js";
|
||||||
@@ -14,13 +14,7 @@ export const CreatePlanParamsV1Schema = z.object({
|
|||||||
add_on: z.boolean().default(false),
|
add_on: z.boolean().default(false),
|
||||||
auto_enable: z.boolean().default(false),
|
auto_enable: z.boolean().default(false),
|
||||||
|
|
||||||
price: z
|
price: BasePriceParamsSchema.optional(),
|
||||||
.object({
|
|
||||||
amount: z.number(),
|
|
||||||
interval: z.enum(BillingInterval),
|
|
||||||
interval_count: z.number().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
items: z.array(CreatePlanItemParamsV1Schema).optional(),
|
items: z.array(CreatePlanItemParamsV1Schema).optional(),
|
||||||
free_trial: FreeTrialParamsV1Schema.optional(),
|
free_trial: FreeTrialParamsV1Schema.optional(),
|
||||||
|
|||||||
@@ -45,11 +45,19 @@ export const CreatePlanItemParamsV1Schema = z
|
|||||||
|
|
||||||
rollover: z
|
rollover: z
|
||||||
.object({
|
.object({
|
||||||
max: z.number(),
|
max: z.number().optional(),
|
||||||
expiry_duration_type: z.enum(RolloverExpiryDurationType),
|
expiry_duration_type: z.enum(RolloverExpiryDurationType),
|
||||||
expiry_duration_length: z.number().optional(),
|
expiry_duration_length: z.number().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
entitlement_id: z.string().optional().meta({
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
|
price_id: z.string().optional().meta({
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.check((ctx) => {
|
.check((ctx) => {
|
||||||
const resetInterval = ctx.value.reset?.interval;
|
const resetInterval = ctx.value.reset?.interval;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function planItemParamsV1ToPlanItemV0({
|
|||||||
|
|
||||||
rollover: item.rollover
|
rollover: item.rollover
|
||||||
? {
|
? {
|
||||||
max: item.rollover.max,
|
max: item.rollover.max ?? null,
|
||||||
expiry_duration_type: item.rollover.expiry_duration_type,
|
expiry_duration_type: item.rollover.expiry_duration_type,
|
||||||
expiry_duration_length: item.rollover.expiry_duration_length,
|
expiry_duration_length: item.rollover.expiry_duration_length,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ export const planItemV0ToProductItem = ({
|
|||||||
? ProductItemType.FeaturePrice
|
? ProductItemType.FeaturePrice
|
||||||
: ProductItemType.Feature;
|
: ProductItemType.Feature;
|
||||||
|
|
||||||
|
const entitlementId =
|
||||||
|
"entitlement_id" in planItem ? planItem.entitlement_id : undefined;
|
||||||
|
const priceId = "price_id" in planItem ? planItem.price_id : undefined;
|
||||||
|
|
||||||
return ProductItemSchema.parse({
|
return ProductItemSchema.parse({
|
||||||
type,
|
type,
|
||||||
|
|
||||||
@@ -155,5 +159,8 @@ export const planItemV0ToProductItem = ({
|
|||||||
config,
|
config,
|
||||||
|
|
||||||
display: "display" in planItem ? planItem.display : undefined,
|
display: "display" in planItem ? planItem.display : undefined,
|
||||||
|
|
||||||
|
entitlement_id: entitlementId,
|
||||||
|
price_id: priceId,
|
||||||
} satisfies ProductItem);
|
} satisfies ProductItem);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { billingMethodToUsageModel } from "@api/products/components/mappers/billingMethodTousageModel.js";
|
import { billingMethodToUsageModel } from "@api/products/components/mappers/billingMethodTousageModel.js";
|
||||||
import type { ApiPlanItemV0 } from "@api/products/items/previousVersions/apiPlanItemV0.js";
|
import type { ApiPlanItemV0 } from "@api/products/items/previousVersions/apiPlanItemV0.js";
|
||||||
import type { ApiPlanItemV1 } from "../apiPlanItemV1.js";
|
import type { ApiPlanItemV1 } from "../apiPlanItemV1.js";
|
||||||
|
import type { CreatePlanItemParamsV1 } from "../crud/createPlanItemParamsV1.js";
|
||||||
|
|
||||||
/** Transform ApiPlanItemV1 to ApiPlanItemV0 */
|
/** Transform ApiPlanItemV1 to ApiPlanItemV0 */
|
||||||
export function planItemV1ToV0(item: ApiPlanItemV1): ApiPlanItemV0 {
|
export function planItemV1ToV0(
|
||||||
const { included, price, ...restItem } = item;
|
item: ApiPlanItemV1 | CreatePlanItemParamsV1,
|
||||||
|
): ApiPlanItemV0 {
|
||||||
|
const { included = 0, price, ...restItem } = item;
|
||||||
|
|
||||||
|
const billingUnits = price?.billing_units ?? 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...restItem,
|
...restItem,
|
||||||
|
unlimited: item.unlimited ?? false,
|
||||||
granted_balance: included,
|
granted_balance: included,
|
||||||
reset: item.reset
|
reset: item.reset
|
||||||
? {
|
? {
|
||||||
@@ -21,10 +28,21 @@ export function planItemV1ToV0(item: ApiPlanItemV1): ApiPlanItemV0 {
|
|||||||
tiers: price.tiers,
|
tiers: price.tiers,
|
||||||
interval: price.interval,
|
interval: price.interval,
|
||||||
interval_count: price.interval_count,
|
interval_count: price.interval_count,
|
||||||
billing_units: price.billing_units,
|
billing_units: billingUnits,
|
||||||
usage_model: billingMethodToUsageModel(price.billing_method),
|
usage_model: billingMethodToUsageModel(price.billing_method),
|
||||||
max_purchase: price.max_purchase,
|
max_purchase: price.max_purchase ?? null,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
|
||||||
|
rollover: item.rollover
|
||||||
|
? {
|
||||||
|
max: item.rollover.max ?? null,
|
||||||
|
expiry_duration_type: item.rollover.expiry_duration_type,
|
||||||
|
expiry_duration_length: item.rollover.expiry_duration_length,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
entitlement_id: "entitlement_id" in item ? item.entitlement_id : undefined,
|
||||||
|
price_id: "price_id" in item ? item.price_id : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ export const ApiPlanItemV0Schema = z
|
|||||||
on_decrease: z.enum(OnDecrease).optional(),
|
on_decrease: z.enum(OnDecrease).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
|
entitlement_id: z.string().optional().meta({
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
|
price_id: z.string().optional().meta({
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.check((ctx) => {
|
.check((ctx) => {
|
||||||
const resetInterval = ctx.value.reset?.interval;
|
const resetInterval = ctx.value.reset?.interval;
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import type { ApiPlanItemV0 } from "@api/models";
|
|
||||||
import type { ApiPlan } from "@api/products/previousVersions/apiPlanV0";
|
|
||||||
import { BillingInterval } from "@models/productModels/intervals/billingInterval";
|
|
||||||
import {
|
|
||||||
type ProductItem,
|
|
||||||
ProductItemType,
|
|
||||||
} from "@models/productV2Models/productItemModels/productItemModels";
|
|
||||||
import { getProductItemDisplay } from "@utils/productDisplayUtils";
|
|
||||||
import { billingToItemInterval } from "@utils/productV2Utils/productItemUtils/itemIntervalUtils";
|
|
||||||
import type { SharedContext } from "../../../types";
|
|
||||||
|
|
||||||
export const planV0ToBasePriceProductItem = ({
|
|
||||||
ctx,
|
|
||||||
plan,
|
|
||||||
}: {
|
|
||||||
ctx: SharedContext;
|
|
||||||
plan: {
|
|
||||||
features: ApiPlanItemV0[];
|
|
||||||
price: ApiPlan["price"];
|
|
||||||
};
|
|
||||||
}): ProductItem | undefined => {
|
|
||||||
if (!plan.price) return;
|
|
||||||
|
|
||||||
const basePrice = plan.price;
|
|
||||||
|
|
||||||
const basePriceDisplay =
|
|
||||||
"display" in basePrice ? basePrice.display : undefined;
|
|
||||||
|
|
||||||
if (basePrice) {
|
|
||||||
const item = {
|
|
||||||
type: ProductItemType.Price,
|
|
||||||
feature_id: null,
|
|
||||||
feature: null,
|
|
||||||
interval: billingToItemInterval({
|
|
||||||
billingInterval: basePrice.interval ?? BillingInterval.Month,
|
|
||||||
}),
|
|
||||||
interval_count: basePrice.interval_count ?? 1,
|
|
||||||
price: basePrice.amount ?? 0,
|
|
||||||
} satisfies ProductItem;
|
|
||||||
|
|
||||||
const display =
|
|
||||||
basePriceDisplay ??
|
|
||||||
getProductItemDisplay({ item, features: ctx.features });
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
display,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user