Files
cfw-autumn/shared/utils/cusProductUtils/classifyCustomerProduct/classifyCustomerProduct.ts
2026-01-23 08:43:48 +00:00

224 lines
6.9 KiB
TypeScript

import { CusProductStatus } from "@models/cusProductModels/cusProductEnums.js";
import type { FullCusProduct } from "@models/cusProductModels/cusProductModels.js";
import {
isFreeProduct,
isOneOffProduct,
} from "../../productUtils/classifyProduct/classifyProductUtils";
import { notNullish, nullish } from "../../utils";
import { ACTIVE_STATUSES, RELEVANT_STATUSES } from "..";
import { cusProductToPrices } from "../convertCusProduct";
// ============================================================================
// PRODUCT TYPE CHECKS
// ============================================================================
export const isCustomerProductMain = (customerProduct?: FullCusProduct) => {
if (!customerProduct) return false;
return !customerProduct.product.is_add_on;
};
export const isCustomerProductAddOn = (customerProduct?: FullCusProduct) => {
if (!customerProduct) return false;
return customerProduct.product.is_add_on;
};
export const isCustomerProductOneOff = (customerProduct?: FullCusProduct) => {
if (!customerProduct) return false;
const prices = cusProductToPrices({ cusProduct: customerProduct });
return isOneOffProduct({ prices });
};
/** Returns true if the product is recurring (not a one-off). Includes free products. */
export const isCustomerProductRecurring = (
customerProduct?: FullCusProduct,
) => {
if (!customerProduct) return false;
const prices = cusProductToPrices({ cusProduct: customerProduct });
return !isOneOffProduct({ prices });
};
export const isCustomerProductFree = (cusProduct?: FullCusProduct) => {
if (!cusProduct) return false;
const prices = cusProductToPrices({ cusProduct });
return isFreeProduct({ prices });
};
/** Customer product has at least one price that's not free */
export const isCustomerProductPaid = (customerProduct?: FullCusProduct) => {
if (!customerProduct) return false;
const prices = cusProductToPrices({ cusProduct: customerProduct });
return !isFreeProduct({ prices });
};
/** Customer product is both paid AND recurring (not free, not one-off) */
export const isCustomerProductPaidRecurring = (
customerProduct?: FullCusProduct,
) => {
if (!customerProduct) return false;
const prices = cusProductToPrices({ cusProduct: customerProduct });
return !isFreeProduct({ prices }) && !isOneOffProduct({ prices });
};
// ============================================================================
// STATUS CHECKS
// ============================================================================
export const isCustomerProductScheduled = (cp?: FullCusProduct) => {
if (!cp) return false;
return cp.status === CusProductStatus.Scheduled;
};
/**
* Checks if a scheduled customer product should be activated based on its starts_at time.
* Uses an optional tolerance to handle timing differences between Stripe and webhook arrival.
*
* @param toleranceMs - Tolerance in milliseconds (default: 10 minutes)
*/
export const hasCustomerProductStarted = (
cp: FullCusProduct,
params: { nowMs: number; toleranceMs?: number },
) => {
if (cp.status !== CusProductStatus.Scheduled) return false;
const toleranceMs = params.toleranceMs ?? 10 * 60 * 1000; // 10 minutes default
return cp.starts_at <= params.nowMs + toleranceMs;
};
export const isCustomerProductCanceling = (cp?: FullCusProduct) => {
if (!cp) return false;
return notNullish(cp.canceled_at);
};
export const isCustomerProductExpired = (cp?: FullCusProduct) => {
if (!cp) return false;
return cp.status === CusProductStatus.Expired;
};
/**
* Checks if a canceling customer product has reached its end time.
* Uses an optional tolerance to handle timing differences between Stripe and webhook arrival.
*
* @param toleranceMs - Tolerance in milliseconds (default: 10 minutes)
*/
export const hasCustomerProductEnded = (
cp: FullCusProduct,
params?: { nowMs?: number },
) => {
const nowMs = params?.nowMs ?? Date.now();
const hasEnded =
isCustomerProductCanceling(cp) &&
notNullish(cp.ended_at) &&
nowMs >= cp.ended_at;
return hasEnded;
};
export const isCustomerProductTrialing = (
customerProduct?: FullCusProduct,
params?: { nowMs?: number },
) => {
if (!customerProduct) return false;
const nowMs = params?.nowMs ?? Date.now();
return customerProduct.trial_ends_at && customerProduct.trial_ends_at > nowMs;
};
export const customerProductHasRelevantStatus = (cp?: FullCusProduct) => {
if (!cp) return false;
return RELEVANT_STATUSES.includes(cp.status);
};
export const customerProductHasActiveStatus = (cp?: FullCusProduct) => {
if (!cp) return false;
return ACTIVE_STATUSES.includes(cp.status);
};
// ============================================================================
// ENTITY CHECKS
// ============================================================================
/**
* Returns true if the customer product is assigned to the given entity,
* or if no entity is specified, true if the product is not assigned to any entity.
*/
export const isCusProductOnEntity = ({
cusProduct,
internalEntityId,
}: {
cusProduct: FullCusProduct;
internalEntityId?: string;
}) => {
return internalEntityId
? cusProduct.internal_entity_id === internalEntityId
: nullish(cusProduct.internal_entity_id);
};
// ============================================================================
// STRIPE SUBSCRIPTION CHECKS
// ============================================================================
export const isCustomerProductOnStripeSubscription = ({
customerProduct,
stripeSubscriptionId,
}: {
customerProduct: FullCusProduct;
stripeSubscriptionId: string;
}) => {
return customerProduct.subscription_ids?.includes(stripeSubscriptionId);
};
export const isCustomerProductOnStripeSubscriptionSchedule = ({
customerProduct,
stripeSubscriptionScheduleId,
}: {
customerProduct: FullCusProduct;
stripeSubscriptionScheduleId: string;
}) => {
return customerProduct.scheduled_ids?.includes(stripeSubscriptionScheduleId);
};
/** Note: this does not CONFIRM that the subscription is active (might be canceled in Stripe...) */
export const cusProductHasSubscription = ({
cusProduct,
}: {
cusProduct: FullCusProduct;
}) => {
const prices = cusProductToPrices({ cusProduct });
if (isFreeProduct({ prices }) || isOneOffProduct({ prices })) return false;
const subId = cusProduct.subscription_ids?.[0];
return notNullish(subId);
};
export const customerProductHasSubscriptionSchedule = ({
cusProduct,
}: {
cusProduct: FullCusProduct;
}) => {
const prices = cusProductToPrices({ cusProduct });
if (isFreeProduct({ prices }) || isOneOffProduct({ prices })) return false;
const subId = cusProduct.scheduled_ids?.[0];
return notNullish(subId);
};
export const isCustomerProductEntityScoped = (
customerProduct?: FullCusProduct,
) => {
if (!customerProduct) return false;
return notNullish(customerProduct.internal_entity_id);
};
export const isCustomerProductCustomerScoped = (
customerProduct?: FullCusProduct,
) => {
if (!customerProduct) return false;
return nullish(customerProduct.internal_entity_id);
};