247 lines
7.2 KiB
TypeScript
247 lines
7.2 KiB
TypeScript
import RecaseError from "@/utils/errorUtils.js";
|
|
import { ErrCode } from "@/errors/errCodes.js";
|
|
import { StatusCodes } from "http-status-codes";
|
|
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
|
import { AttachBranch, AttachErrCode, UsagePriceConfig } from "@autumn/shared";
|
|
import { AttachBody } from "../models/AttachBody.js";
|
|
import { AttachConfig, AttachFlags } from "../models/AttachFlags.js";
|
|
import {
|
|
getEntOptions,
|
|
priceIsOneOffAndTiered,
|
|
} from "@/internal/products/prices/priceUtils.js";
|
|
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
|
|
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
|
import { BillingType } from "@autumn/shared";
|
|
import { notNullish, nullOrUndefined } from "@/utils/genUtils.js";
|
|
import { attachParamToCusProducts } from "./convertAttachParams.js";
|
|
import {
|
|
cusProductsToCusEnts,
|
|
cusProductToPrices,
|
|
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
|
|
import { findPriceForFeature } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
|
|
import { getResetBalance } from "../../cusProducts/cusEnts/cusEntUtils.js";
|
|
import { Decimal } from "decimal.js";
|
|
|
|
const handleNonCheckoutErrors = ({
|
|
flags,
|
|
action,
|
|
}: {
|
|
flags: AttachFlags;
|
|
action: string;
|
|
}) => {
|
|
const { isPublic, forceCheckout, noPaymentMethod } = flags;
|
|
|
|
if (isPublic) {
|
|
throw new RecaseError({
|
|
message: `Not allowed to ${action} when using publishable key`,
|
|
code: ErrCode.InvalidRequest,
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
});
|
|
} else if (forceCheckout) {
|
|
throw new RecaseError({
|
|
message: `Not allowed to ${action} when using force_checkout`,
|
|
code: ErrCode.InvalidRequest,
|
|
});
|
|
} else if (noPaymentMethod) {
|
|
throw new RecaseError({
|
|
message: `Not allowed to ${action} because customer has no payment method on file`,
|
|
code: ErrCode.InvalidRequest,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handlePrepaidErrors = async ({
|
|
attachParams,
|
|
useCheckout = false,
|
|
}: {
|
|
attachParams: AttachParams;
|
|
useCheckout?: boolean;
|
|
}) => {
|
|
const { prices, entitlements, optionsList } = attachParams;
|
|
|
|
// 2. Check if options are valid
|
|
for (const price of prices) {
|
|
const billingType = getBillingType(price.config!);
|
|
|
|
if (billingType === BillingType.UsageInAdvance) {
|
|
// Get options for price
|
|
let priceEnt = getPriceEntitlement(price, entitlements);
|
|
let options = getEntOptions(optionsList, priceEnt);
|
|
|
|
// 1. If not checkout, quantity should be defined
|
|
if (!useCheckout && nullOrUndefined(options?.quantity)) {
|
|
throw new RecaseError({
|
|
message: `Pass in 'quantity' for feature ${priceEnt.feature_id} in options`,
|
|
code: ErrCode.InvalidOptions,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
if (
|
|
nullOrUndefined(options?.quantity) &&
|
|
priceIsOneOffAndTiered(price, priceEnt)
|
|
) {
|
|
throw new RecaseError({
|
|
code: ErrCode.InvalidRequest,
|
|
message:
|
|
"Quantity is required for start of period price that is one off and tiered",
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
// 3. Quantity cannot be negative
|
|
if (notNullish(options?.quantity) && options?.quantity! < 0) {
|
|
throw new RecaseError({
|
|
message: `Quantity cannot be negative`,
|
|
code: ErrCode.InvalidOptions,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
// 4. If there's only one price, quantity must be greater than 0
|
|
if (options?.quantity === 0 && prices.length === 1) {
|
|
throw new RecaseError({
|
|
message: `When there's only one price, quantity must be greater than 0`,
|
|
code: ErrCode.InvalidOptions,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
let usageLimit = priceEnt.usage_limit;
|
|
let totalQuantity =
|
|
options?.quantity! * (price.config as UsagePriceConfig).billing_units!;
|
|
|
|
if (usageLimit && totalQuantity + priceEnt.allowance! > usageLimit) {
|
|
throw new RecaseError({
|
|
message: `Quantity + included usage exceeds usage limit of ${usageLimit} for feature ${priceEnt.feature_id}`,
|
|
code: ErrCode.InvalidOptions,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleUpdateQuantityErrors = async ({
|
|
attachParams,
|
|
}: {
|
|
attachParams: AttachParams;
|
|
}) => {
|
|
const { curMainProduct, curSameProduct } = attachParamToCusProducts({
|
|
attachParams,
|
|
});
|
|
|
|
if (!curSameProduct && !curMainProduct) {
|
|
return;
|
|
}
|
|
|
|
const cusProduct = curSameProduct || curMainProduct!;
|
|
const cusEnts = cusProductsToCusEnts({ cusProducts: [cusProduct] });
|
|
const prices = cusProductToPrices({ cusProduct });
|
|
|
|
for (const option of attachParams.optionsList) {
|
|
const price = findPriceForFeature({
|
|
prices,
|
|
internalFeatureId: option.internal_feature_id!,
|
|
});
|
|
|
|
if (!price) continue;
|
|
|
|
const totalQuantity =
|
|
option.quantity! * (price?.config as UsagePriceConfig).billing_units!;
|
|
|
|
const totalUsage = cusEnts
|
|
.reduce((acc, curr) => {
|
|
if (
|
|
curr.entitlement.internal_feature_id == option.internal_feature_id
|
|
) {
|
|
const allowance = getResetBalance({
|
|
entitlement: curr.entitlement,
|
|
options: cusProduct.options.find(
|
|
(o) => o.internal_feature_id == option.internal_feature_id,
|
|
),
|
|
relatedPrice: price,
|
|
});
|
|
|
|
const usage = new Decimal(allowance!).minus(curr.balance!);
|
|
|
|
return acc.plus(usage);
|
|
}
|
|
|
|
return acc;
|
|
}, new Decimal(0))
|
|
.toNumber();
|
|
|
|
if (totalUsage > totalQuantity) {
|
|
throw new RecaseError({
|
|
message: `Current usage for ${option.feature_id} is ${totalUsage}, can't update to ${totalQuantity}`,
|
|
code: AttachErrCode.InvalidOptions,
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
export const handleAttachErrors = async ({
|
|
attachParams,
|
|
attachBody,
|
|
branch,
|
|
flags,
|
|
config,
|
|
}: {
|
|
attachParams: AttachParams;
|
|
attachBody: AttachBody;
|
|
branch: AttachBranch;
|
|
flags: AttachFlags;
|
|
config: AttachConfig;
|
|
}) => {
|
|
const { onlyCheckout } = config;
|
|
|
|
if (onlyCheckout || flags.isPublic) {
|
|
let upgradeDowngradeFlows = [
|
|
AttachBranch.Upgrade,
|
|
AttachBranch.Downgrade,
|
|
AttachBranch.MainIsTrial,
|
|
];
|
|
if (upgradeDowngradeFlows.includes(branch)) {
|
|
handleNonCheckoutErrors({
|
|
flags,
|
|
action: "perform upgrade or downgrade",
|
|
});
|
|
}
|
|
let updateProductFlows = [
|
|
AttachBranch.NewVersion,
|
|
AttachBranch.SameCustom,
|
|
AttachBranch.UpdatePrepaidQuantity,
|
|
AttachBranch.Renew,
|
|
];
|
|
if (updateProductFlows.includes(branch)) {
|
|
handleNonCheckoutErrors({
|
|
flags,
|
|
action: "update current product",
|
|
});
|
|
}
|
|
}
|
|
|
|
// 2. If same custom ents, not allowed if is public flow...
|
|
if (branch == AttachBranch.SameCustomEnts) {
|
|
if (flags.isPublic) {
|
|
throw new RecaseError({
|
|
message:
|
|
"Not allowed to update current product when using publishable key",
|
|
code: ErrCode.InvalidRequest,
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
});
|
|
}
|
|
}
|
|
|
|
await handlePrepaidErrors({
|
|
attachParams,
|
|
useCheckout: onlyCheckout,
|
|
});
|
|
|
|
// await handleUpdateQuantityErrors({
|
|
// attachParams,
|
|
// });
|
|
};
|