372 lines
7.4 KiB
TypeScript
372 lines
7.4 KiB
TypeScript
import {
|
|
AllowanceType,
|
|
type AppEnv,
|
|
CusProductStatus,
|
|
type Customer,
|
|
type Feature,
|
|
FeatureType,
|
|
type FullCustomerEntitlement,
|
|
type Organization,
|
|
} from "@autumn/shared";
|
|
import { Decimal } from "decimal.js";
|
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
|
import { CusService } from "@/internal/customers/CusService.js";
|
|
import { refreshCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
|
import { getFeatureBalance } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
|
import { deductFromApiCusRollovers } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.js";
|
|
import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js";
|
|
import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js";
|
|
import { handleThresholdReached } from "./handleThresholdReached.js";
|
|
import {
|
|
deductAllowanceFromCusEnt,
|
|
deductFromUsageBasedCusEnt,
|
|
} from "./updateBalanceTask.js";
|
|
|
|
// 2. Get deductions for each feature
|
|
const getFeatureDeductions = ({
|
|
cusEnts,
|
|
value,
|
|
features,
|
|
shouldSet,
|
|
}: {
|
|
cusEnts: FullCustomerEntitlement[];
|
|
value: number;
|
|
features: Feature[];
|
|
shouldSet: boolean;
|
|
}) => {
|
|
const meteredFeature =
|
|
features.find((f) => f.type === FeatureType.Metered) || features[0];
|
|
|
|
const featureDeductions = [];
|
|
for (const feature of features) {
|
|
let newValue = value;
|
|
const unlimitedExists = cusEnts.some(
|
|
(cusEnt) =>
|
|
cusEnt.entitlement.allowance_type === AllowanceType.Unlimited &&
|
|
cusEnt.entitlement.internal_feature_id == feature.internal_id,
|
|
);
|
|
|
|
if (unlimitedExists) {
|
|
continue;
|
|
}
|
|
|
|
if (feature.type === FeatureType.CreditSystem) {
|
|
newValue = featureToCreditSystem({
|
|
featureId: meteredFeature.id,
|
|
creditSystem: feature,
|
|
amount: value,
|
|
});
|
|
}
|
|
|
|
// If it's set
|
|
let deduction = newValue;
|
|
|
|
if (shouldSet) {
|
|
const totalAllowance = cusEnts.reduce((acc, curr) => {
|
|
return acc + (curr.entitlement.allowance || 0);
|
|
}, 0);
|
|
|
|
const targetBalance = new Decimal(totalAllowance).sub(value).toNumber();
|
|
|
|
const totalBalance = getFeatureBalance({
|
|
cusEnts,
|
|
internalFeatureId: feature.internal_id!,
|
|
})!;
|
|
|
|
deduction = new Decimal(totalBalance).sub(targetBalance).toNumber();
|
|
}
|
|
|
|
if (deduction == 0) {
|
|
console.log(` - Skipping feature ${feature.id} -- deduction is 0`);
|
|
continue;
|
|
}
|
|
|
|
featureDeductions.push({
|
|
feature,
|
|
deduction,
|
|
});
|
|
}
|
|
|
|
featureDeductions.sort((a, b) => {
|
|
if (
|
|
a.feature.type === FeatureType.CreditSystem &&
|
|
b.feature.type !== FeatureType.CreditSystem
|
|
) {
|
|
return 1;
|
|
}
|
|
|
|
if (
|
|
a.feature.type !== FeatureType.CreditSystem &&
|
|
b.feature.type === FeatureType.CreditSystem
|
|
) {
|
|
return -1;
|
|
}
|
|
|
|
return a.feature.id.localeCompare(b.feature.id);
|
|
});
|
|
|
|
return featureDeductions;
|
|
};
|
|
|
|
const logUsageUpdate = ({
|
|
customer,
|
|
features,
|
|
cusEnts,
|
|
featureDeductions,
|
|
org,
|
|
setUsage,
|
|
entityId,
|
|
}: {
|
|
customer: Customer;
|
|
features: Feature[];
|
|
cusEnts: FullCustomerEntitlement[];
|
|
featureDeductions: any;
|
|
org: Organization;
|
|
setUsage: boolean;
|
|
entityId?: string;
|
|
}) => {
|
|
console.log(
|
|
` - Customer: ${customer.id} (${customer.env}) | Org: ${
|
|
org.slug
|
|
} | Features: ${features.map((f) => f.id).join(", ")} | Set Usage: ${
|
|
setUsage ? "true" : "false"
|
|
}`,
|
|
);
|
|
|
|
console.log(
|
|
" - CusEnts:",
|
|
cusEnts.map((cusEnt: any) => {
|
|
let balanceStr = cusEnt.balance;
|
|
try {
|
|
if (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited) {
|
|
balanceStr = "Unlimited";
|
|
}
|
|
} catch (error) {
|
|
balanceStr = "failed_to_get_balance";
|
|
}
|
|
|
|
if (entityId && cusEnt.entities) {
|
|
balanceStr = `${cusEnt.entities?.[entityId!]?.balance} [${entityId}]`;
|
|
}
|
|
|
|
return `${cusEnt.feature_id} - ${balanceStr} (${
|
|
cusEnt.customer_product ? cusEnt.customer_product.product_id : ""
|
|
})`;
|
|
}),
|
|
"| Deductions:",
|
|
featureDeductions.map((f: any) => `${f.feature.id}: ${f.deduction}`),
|
|
);
|
|
};
|
|
|
|
// Main function to update customer balance
|
|
export const updateUsage = async ({
|
|
db,
|
|
customerId,
|
|
features,
|
|
org,
|
|
env,
|
|
value,
|
|
properties,
|
|
setUsage,
|
|
logger,
|
|
entityId,
|
|
allFeatures,
|
|
}: {
|
|
db: DrizzleCli;
|
|
customerId: string;
|
|
features: Feature[];
|
|
org: Organization;
|
|
env: AppEnv;
|
|
value: number;
|
|
properties: any;
|
|
setUsage: boolean;
|
|
logger: any;
|
|
entityId?: string;
|
|
allFeatures: Feature[];
|
|
}) => {
|
|
const customer = await CusService.getFull({
|
|
db,
|
|
idOrInternalId: customerId,
|
|
orgId: org.id,
|
|
env,
|
|
inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
|
|
entityId,
|
|
withSubs: true,
|
|
});
|
|
|
|
const { cusEnts, cusPrices } = await getCusEntsInFeatures({
|
|
customer,
|
|
internalFeatureIds: features.map((f) => f.internal_id!),
|
|
logger,
|
|
reverseOrder: org.config?.reverse_deduction_order,
|
|
});
|
|
|
|
// 1. Get deductions for each feature
|
|
const featureDeductions = getFeatureDeductions({
|
|
cusEnts,
|
|
value,
|
|
shouldSet: setUsage,
|
|
features,
|
|
});
|
|
|
|
logUsageUpdate({
|
|
customer,
|
|
features,
|
|
cusEnts,
|
|
featureDeductions,
|
|
org,
|
|
setUsage,
|
|
entityId,
|
|
});
|
|
|
|
// 3. Return if no customer entitlements or features found
|
|
if (cusEnts.length === 0 || features.length === 0) {
|
|
console.log(" - No customer entitlements or features found");
|
|
return;
|
|
}
|
|
|
|
const originalCusEnts = structuredClone(cusEnts);
|
|
for (const obj of featureDeductions) {
|
|
let { feature, deduction: toDeduct } = obj;
|
|
|
|
for (const cusEnt of cusEnts) {
|
|
if (cusEnt.entitlement.internal_feature_id != feature.internal_id) {
|
|
continue;
|
|
}
|
|
|
|
toDeduct = await deductFromApiCusRollovers({
|
|
toDeduct,
|
|
cusEnt,
|
|
deductParams: {
|
|
db,
|
|
feature,
|
|
env,
|
|
entity: customer.entity ? customer.entity : undefined,
|
|
},
|
|
});
|
|
|
|
if (toDeduct == 0) continue;
|
|
|
|
toDeduct = await deductAllowanceFromCusEnt({
|
|
toDeduct,
|
|
cusEnt,
|
|
deductParams: {
|
|
db,
|
|
feature,
|
|
env,
|
|
org,
|
|
cusPrices: cusPrices as any[],
|
|
customer,
|
|
properties,
|
|
entity: customer.entity,
|
|
},
|
|
featureDeductions,
|
|
willDeductCredits: true,
|
|
setZeroAdjustment: true,
|
|
});
|
|
}
|
|
|
|
if (toDeduct !== 0) {
|
|
await deductFromUsageBasedCusEnt({
|
|
toDeduct,
|
|
cusEnts,
|
|
deductParams: {
|
|
db,
|
|
feature,
|
|
env,
|
|
org,
|
|
cusPrices: cusPrices as any[],
|
|
customer,
|
|
properties,
|
|
entity: customer.entity,
|
|
},
|
|
setZeroAdjustment: true,
|
|
});
|
|
}
|
|
|
|
handleThresholdReached({
|
|
org,
|
|
env,
|
|
features: allFeatures,
|
|
db,
|
|
|
|
feature,
|
|
cusEnts: originalCusEnts,
|
|
newCusEnts: cusEnts,
|
|
fullCus: customer,
|
|
logger,
|
|
});
|
|
}
|
|
|
|
return cusEnts;
|
|
};
|
|
|
|
// MAIN FUNCTION
|
|
export const runUpdateUsageTask = async ({
|
|
payload,
|
|
logger,
|
|
db,
|
|
throwError = false,
|
|
}: {
|
|
payload: any;
|
|
logger: any;
|
|
db: DrizzleCli;
|
|
throwError?: boolean;
|
|
}) => {
|
|
try {
|
|
// 1. Update customer balance
|
|
const {
|
|
internalCustomerId,
|
|
customerId,
|
|
eventId,
|
|
features,
|
|
value,
|
|
set_usage,
|
|
properties,
|
|
org,
|
|
env,
|
|
entityId,
|
|
allFeatures,
|
|
} = payload;
|
|
|
|
console.log("--------------------------------");
|
|
console.log(
|
|
`HANDLING USAGE TASK FOR CUSTOMER (${customerId}), ORG: ${org.slug}, EVENT ID: ${eventId}`,
|
|
);
|
|
|
|
const cusEnts: any = await updateUsage({
|
|
db,
|
|
customerId,
|
|
features,
|
|
value,
|
|
properties,
|
|
org,
|
|
env,
|
|
setUsage: set_usage,
|
|
logger,
|
|
entityId,
|
|
allFeatures,
|
|
});
|
|
|
|
await refreshCusCache({
|
|
db,
|
|
customerId,
|
|
entityId,
|
|
org,
|
|
env,
|
|
});
|
|
|
|
if (!cusEnts || cusEnts.length === 0) {
|
|
return;
|
|
}
|
|
console.log(" ✅ Customer balance updated");
|
|
} catch (error) {
|
|
logger.error(`ERROR UPDATING USAGE`);
|
|
logger.error(error);
|
|
|
|
if (throwError) {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|