224 lines
6.9 KiB
TypeScript
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);
|
|
};
|