120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
import { isEntityCusEnt } from "@utils/cusEntUtils/cusEntUtils.js";
|
|
import type { Entity } from "../../../models/cusModels/entityModels/entityModels.js";
|
|
import type { FullCustomer } from "../../../models/cusModels/fullCusModel.js";
|
|
import type { CustomerEntitlementFilters } from "../../../models/cusProductModels/cusEntModels/cusEntModels.js";
|
|
import type { FullCusEntWithFullCusProduct } from "../../../models/cusProductModels/cusEntModels/cusEntWithProduct.js";
|
|
import { CusProductStatus } from "../../../models/cusProductModels/cusProductEnums.js";
|
|
import { cusEntMatchesEntity } from "../../cusEntUtils/filterCusEntUtils.js";
|
|
import { sortCusEntsForDeduction } from "../../cusEntUtils/sortCusEntsForDeduction.js";
|
|
import { notNullish } from "../../utils.js";
|
|
|
|
export const fullCustomerToCustomerEntitlements = ({
|
|
fullCustomer,
|
|
inStatuses = [CusProductStatus.Active, CusProductStatus.PastDue],
|
|
reverseOrder = false,
|
|
featureId,
|
|
featureIds,
|
|
entity,
|
|
customerEntitlementFilters,
|
|
isRefund = false,
|
|
}: {
|
|
fullCustomer: FullCustomer;
|
|
inStatuses?: CusProductStatus[];
|
|
reverseOrder?: boolean;
|
|
featureId?: string;
|
|
featureIds?: string[];
|
|
entity?: Entity;
|
|
customerEntitlementFilters?: CustomerEntitlementFilters;
|
|
isRefund?: boolean;
|
|
}) => {
|
|
const cusProducts = fullCustomer.customer_products;
|
|
let cusEnts: FullCusEntWithFullCusProduct[] = [];
|
|
|
|
for (const cusProduct of cusProducts) {
|
|
if (!inStatuses.includes(cusProduct.status)) continue;
|
|
|
|
cusEnts.push(
|
|
...cusProduct.customer_entitlements.map((cusEnt) => ({
|
|
...cusEnt,
|
|
customer_product: cusProduct,
|
|
})),
|
|
);
|
|
}
|
|
|
|
for (const cusEnt of fullCustomer.extra_customer_entitlements || []) {
|
|
cusEnts.push({
|
|
...cusEnt,
|
|
customer_product: null,
|
|
});
|
|
}
|
|
|
|
if (featureId) {
|
|
cusEnts = cusEnts.filter(
|
|
(cusEnt) => cusEnt.entitlement.feature.id === featureId,
|
|
);
|
|
}
|
|
|
|
if (featureIds) {
|
|
cusEnts = cusEnts.filter((cusEnt) =>
|
|
featureIds.includes(cusEnt.entitlement.feature.id),
|
|
);
|
|
}
|
|
|
|
if (entity) {
|
|
cusEnts = cusEnts.filter((cusEnt) =>
|
|
cusEntMatchesEntity({
|
|
cusEnt: cusEnt,
|
|
entity,
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Filter out expired entitlements (applies to loose entitlements with expires_at)
|
|
// This is necessary because cached fullCustomer may contain entitlements that have since expired
|
|
const now = Date.now();
|
|
cusEnts = cusEnts.filter(
|
|
(cusEnt) => !cusEnt.expires_at || cusEnt.expires_at > now,
|
|
);
|
|
|
|
sortCusEntsForDeduction({
|
|
cusEnts,
|
|
reverseOrder,
|
|
entityId: entity?.id ?? undefined,
|
|
customerEntitlementFilters,
|
|
});
|
|
|
|
if (
|
|
customerEntitlementFilters?.cusEntIds &&
|
|
customerEntitlementFilters.cusEntIds.length > 0
|
|
) {
|
|
cusEnts = cusEnts.filter((cusEnt) =>
|
|
customerEntitlementFilters.cusEntIds?.includes(cusEnt.id),
|
|
);
|
|
}
|
|
|
|
if (notNullish(customerEntitlementFilters?.interval)) {
|
|
cusEnts = cusEnts.filter(
|
|
(cusEnt) =>
|
|
cusEnt.entitlement.interval === customerEntitlementFilters.interval,
|
|
);
|
|
}
|
|
|
|
if (notNullish(customerEntitlementFilters?.balanceId)) {
|
|
cusEnts = cusEnts.filter(
|
|
(cusEnt) =>
|
|
(cusEnt.external_id ?? cusEnt.id) ===
|
|
customerEntitlementFilters.balanceId,
|
|
);
|
|
}
|
|
|
|
// When disable_pooled_balance is enabled and we're scoped to an entity, drop
|
|
// customer-level (shared pool) cusEnts so the returned balances reflect
|
|
// only the entity's own pool. Matches the filter in prepareFeatureDeduction
|
|
// so the deduction path and reporting stay consistent.
|
|
if (fullCustomer.entity?.id && fullCustomer.config?.disable_pooled_balance) {
|
|
cusEnts = cusEnts.filter((ce) => isEntityCusEnt({ cusEnt: ce }));
|
|
}
|
|
|
|
return cusEnts as FullCusEntWithFullCusProduct[];
|
|
};
|