fix: updated create and remove entity

This commit is contained in:
John Yeo
2025-03-31 21:03:42 +01:00
parent 20a87075db
commit 3cd2baedbc
27 changed files with 619 additions and 199 deletions

13
package-lock.json generated
View File

@@ -9886,6 +9886,18 @@
"integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/csv-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
"integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==",
"license": "MIT",
"bin": {
"csv-parser": "bin/csv-parser"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/currency-symbol-map": { "node_modules/currency-symbol-map": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/currency-symbol-map/-/currency-symbol-map-5.1.0.tgz", "resolved": "https://registry.npmjs.org/currency-symbol-map/-/currency-symbol-map-5.1.0.tgz",
@@ -21282,6 +21294,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^3.5.0", "cron": "^3.5.0",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"csv-parser": "^3.2.0",
"currency-symbol-map": "^5.1.0", "currency-symbol-map": "^5.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"decimal.js": "^10.5.0", "decimal.js": "^10.5.0",

View File

@@ -51,6 +51,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^3.5.0", "cron": "^3.5.0",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"csv-parser": "^3.2.0",
"currency-symbol-map": "^5.1.0", "currency-symbol-map": "^5.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"decimal.js": "^10.5.0", "decimal.js": "^10.5.0",

View File

@@ -49,6 +49,58 @@ export const handleAttachRaceCondition = async ({
} }
}; };
export const handleCustomerRaceCondition = async ({
action,
customerId,
orgId,
env,
res,
logger,
}: {
action: any;
customerId: string;
orgId: string;
env: string;
res: any;
logger: any;
}) => {
const redisConn = await QueueManager.getConnection({ useBackup: false });
try {
const lockKey = `${action}_${customerId}_${orgId}_${env}`;
const existingLock = await redisConn.get(lockKey);
if (existingLock) {
throw new RecaseError({
message: `Action ${action} already running for customer ${customerId}, try again in a few seconds`,
code: ErrCode.InvalidRequest,
statusCode: 400,
});
}
// Create lock with 5 second timeout
await redisConn.set(lockKey, "1", "PX", 5000, "NX");
let originalJson = res.json;
res.json = async function (body: any) {
try {
await clearLock({ lockKey, logger });
} catch (error) {
logger.warn("❗️❗️ Error clearing lock");
logger.warn(error);
}
originalJson.call(this, body);
};
return lockKey;
} catch (error) {
if (error instanceof RecaseError) {
throw error;
}
logger.warn("❗️❗️ Error acquiring lock");
logger.warn(error);
return null;
}
};
export const clearLock = async ({ export const clearLock = async ({
lockKey, lockKey,
logger, logger,

View File

@@ -11,6 +11,7 @@ import {
AllowanceType, AllowanceType,
EntitlementWithFeature, EntitlementWithFeature,
Feature, Feature,
FullCustomerEntitlement,
} from "@autumn/shared"; } from "@autumn/shared";
import RecaseError from "@/utils/errorUtils.js"; import RecaseError from "@/utils/errorUtils.js";
@@ -39,7 +40,7 @@ import {
createStripeOneOffTieredProduct, createStripeOneOffTieredProduct,
} from "./createStripePrice.js"; } from "./createStripePrice.js";
import { getExistingUsageFromCusProducts } from "@/internal/customers/entitlements/cusEntUtils.js"; import { getCusEntMasterBalance, getExistingUsageFromCusProducts } from "@/internal/customers/entitlements/cusEntUtils.js";
import { import {
priceToInArrearProrated, priceToInArrearProrated,
priceToUsageInAdvance, priceToUsageInAdvance,
@@ -266,8 +267,11 @@ export const getStripeSubItems = async ({
const existingUsage = getExistingUsageFromCusProducts({ const existingUsage = getExistingUsageFromCusProducts({
entitlement: priceEnt, entitlement: priceEnt,
cusProducts: attachParams.cusProducts, cusProducts: attachParams.cusProducts,
entities: attachParams.entities,
}); });
if ( if (
billingType == BillingType.UsageInArrear || billingType == BillingType.UsageInArrear ||
billingType == BillingType.InArrearProrated || billingType == BillingType.InArrearProrated ||

View File

@@ -19,6 +19,7 @@ import { AppEnv } from "@autumn/shared";
import { createSupabaseClient } from "./external/supabaseUtils.js"; import { createSupabaseClient } from "./external/supabaseUtils.js";
import { createLogtail } from "./external/logtail/logtailUtils.js"; import { createLogtail } from "./external/logtail/logtailUtils.js";
import { format } from "date-fns"; import { format } from "date-fns";
import { handleRequestError } from "./utils/errorUtils.js";
const init = async () => { const init = async () => {
const app = express(); const app = express();
@@ -91,3 +92,14 @@ const init = async () => {
}; };
init(); init();
process.on("unhandledRejection", (reason, promise) => {
try {
const logtail = createLogtail();
logtail.error("❗️❗️❗️ UNHANDLED REJECTION");
logtail.error(reason);
logtail.flush();
} catch (error) {
console.log("Unhandled rejection", error);
}
});

View File

@@ -29,6 +29,7 @@ import { handleAddCouponToCus } from "./handlers/handleAddCouponToCus.js";
import { handlePostCustomerRequest } from "./handlers/handleCreateCustomer.js"; import { handlePostCustomerRequest } from "./handlers/handleCreateCustomer.js";
import { notNullish } from "@/utils/genUtils.js"; import { notNullish } from "@/utils/genUtils.js";
import { entityRouter } from "../entities/entityRouter.js";
export const cusRouter = Router(); export const cusRouter = Router();
@@ -388,3 +389,5 @@ cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => {
// Invoice // Invoice
cusRouter.post("/:customer_id/coupons/:coupon_id", handleAddCouponToCus); cusRouter.post("/:customer_id/coupons/:coupon_id", handleAddCouponToCus);
cusRouter.use("/:customer_id/entities", entityRouter);

View File

@@ -174,6 +174,7 @@ export const createNewCustomer = async ({
optionsList: [], optionsList: [],
cusProducts: [], cusProducts: [],
invoiceOnly: true, invoiceOnly: true,
entities: [],
}, },
fromRequest: false, fromRequest: false,
}); });

View File

@@ -5,11 +5,13 @@ export class EntityService {
static async getById({ static async getById({
sb, sb,
entityId, entityId,
internalCustomerId,
orgId, orgId,
env, env,
}: { }: {
sb: SupabaseClient; sb: SupabaseClient;
entityId: string; entityId: string;
internalCustomerId?: string;
orgId: string; orgId: string;
env: string; env: string;
}) { }) {
@@ -17,6 +19,7 @@ export class EntityService {
.from("entities") .from("entities")
.select("*") .select("*")
.eq("id", entityId) .eq("id", entityId)
.eq("internal_customer_id", internalCustomerId)
.eq("org_id", orgId) .eq("org_id", orgId)
.eq("env", env) .eq("env", env)
.single(); .single();
@@ -38,26 +41,31 @@ export class EntityService {
return data; return data;
} }
static async getInIds({ static async get({
sb, sb,
ids,
orgId, orgId,
internalFeatureId, internalFeatureId,
internalCustomerId,
env, env,
}: { }: {
sb: SupabaseClient; sb: SupabaseClient;
ids: string[];
orgId: string; orgId: string;
env: string; env: string;
internalFeatureId?: string; internalFeatureId?: string;
internalCustomerId?: string;
}) { }) {
const { data, error } = await sb let query = sb
.from("entities") .from("entities")
.select("*") .select("*")
.in("id", ids)
.eq("org_id", orgId) .eq("org_id", orgId)
.eq("env", env) .eq("env", env)
.eq("internal_feature_id", internalFeatureId); .eq("internal_customer_id", internalCustomerId);
if (internalFeatureId) {
query = query.eq("internal_feature_id", internalFeatureId);
}
const { data, error } = await query;
if (error) { if (error) {
throw error; throw error;

View File

@@ -2,7 +2,7 @@ import { Router } from "express";
import { handleCreateEntity } from "./handleCreateEntity.js"; import { handleCreateEntity } from "./handleCreateEntity.js";
import { handleDeleteEntity } from "./handleDeleteEntity.js"; import { handleDeleteEntity } from "./handleDeleteEntity.js";
export const entityRouter = Router(); export const entityRouter = Router({ mergeParams: true });
// 1. Create entity // 1. Create entity
entityRouter.post("", handleCreateEntity); entityRouter.post("", handleCreateEntity);

View File

@@ -6,9 +6,16 @@ import { OrgService } from "@/internal/orgs/OrgService.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { getLinkedCusEnt } from "./entityUtils.js"; import { getLinkedCusEnt } from "./entityUtils.js";
import { EntityService } from "./EntityService.js"; import { EntityService } from "./EntityService.js";
import { AppEnv, CusProductStatus, Entity } from "@autumn/shared"; import { AppEnv, CusProductStatus, Customer, Entity, FullCusProduct, FullCustomerEntitlement, FullCustomerPrice, Product } from "@autumn/shared";
import { generateId } from "@/utils/genUtils.js"; import { generateId } from "@/utils/genUtils.js";
import { adjustAllowance } from "@/trigger/adjustAllowance.js"; import { adjustAllowance } from "@/trigger/adjustAllowance.js";
import { getActiveCusProductStatuses } from "@/utils/constants.js";
import { isTrialing } from "@/internal/customers/products/cusProductUtils.js";
import {Decimal} from "decimal.js";
import { getPriceForOverage } from "@/internal/prices/priceUtils.js";
import { Logger } from "@slack/web-api";
import Stripe from "stripe";
import { getCusEntMasterBalance } from "@/internal/customers/entitlements/cusEntUtils.js";
export const constructEntity = ({ export const constructEntity = ({
inputEntity, inputEntity,
@@ -55,25 +62,30 @@ export const getEntityToAction = ({
// 1. GET ENTITY TO ACTION // 1. GET ENTITY TO ACTION
let entityToAction: any = {}; let entityToAction: any = {};
let createCount = 0; let createCount = 0;
let replacedEntities: string[] = [];
for (const inputEntity of inputEntities) { for (const inputEntity of inputEntities) {
let curEntity = existingEntities.find((e: any) => e.id === inputEntity.id); let curEntity = existingEntities.find((e: any) => e.id === inputEntity.id);
if (curEntity && !curEntity.deleted) { if (curEntity && curEntity.deleted) {
// Replace // Replace
entityToAction[inputEntity.id] = { entityToAction[inputEntity.id] = {
action: "replace", action: "replace",
replace: curEntity, replace: curEntity,
entity: inputEntity, entity: inputEntity,
}; };
replacedEntities.push(curEntity.id);
continue;
} }
let replaced = false; let replaced = false;
for (const entity of existingEntities) { for (const entity of existingEntities) {
if ( if (
entity.deleted && entity.deleted &&
!Object.keys(entityToAction).some((id) => id === entity.id) !replacedEntities.includes(entity.id)
) { ) {
replaced = true; replaced = true;
replacedEntities.push(entity.id);
entityToAction[inputEntity.id] = { entityToAction[inputEntity.id] = {
action: "replace", action: "replace",
replace: entity, replace: entity,
@@ -87,7 +99,7 @@ export const getEntityToAction = ({
// Create // Create
entityToAction[inputEntity.id] = { entityToAction[inputEntity.id] = {
action: "create", action: "create",
inputEntity, entity: inputEntity,
}; };
createCount++; createCount++;
} }
@@ -121,12 +133,106 @@ export const getEntityToAction = ({
return entityToAction; return entityToAction;
}; };
// export const payForEntitiesImmediately = async ({
// sb,
// env,
// org,
// cusProduct,
// cusEnt,
// cusPrice,
// logger,
// oldUsage,
// createdNumber,
// stripeCli,
// product,
// customer,
// }:{
// cusProduct: FullCusProduct;
// cusEnt: FullCustomerEntitlement;
// cusPrice: FullCustomerPrice;
// logger: Logger;
// oldUsage: number;
// createdNumber: number;
// stripeCli: Stripe;
// product: Product;
// customer: Customer;
// }) => {
// if (!isTrialing(cusProduct as FullCusProduct)) {
// // let entitlement = cusEnt.entitlement;
// // let newUsage = entitlement.allowance! - newBalance;
// // let oldUsage = entitlement.allowance! - originalBalance;
// // newUsage = newUsage - (replacedCount || 0);
// // let newAmount = getPriceForOverage(cusPrice.price, newUsage);
// // let oldAmount = getPriceForOverage(cusPrice.price, oldUsage);
// const stripeAmount = new Decimal(newAmount)
// .sub(oldAmount)
// .mul(100)
// .round()
// .toNumber();
// logger.info(` - Stripe amount: ${stripeAmount}`);
// if (stripeAmount > 0) {
// const invoice = await stripeCli.invoices.create({
// customer: customer.processor.id,
// auto_advance: false,
// subscription: sub.id,
// });
// await stripeCli.invoiceItems.create({
// customer: customer.processor.id,
// invoice: invoice.id,
// quantity: 1,
// description: `${product!.name} - ${
// affectedFeature.name
// } x ${Math.round(newUsage - oldUsage)}`,
// price_data: {
// product: config.stripe_product_id!,
// unit_amount: stripeAmount,
// currency: org.default_currency,
// },
// });
// const { paid, error } = await payForInvoice({
// fullOrg: org,
// env,
// customer,
// invoice,
// logger,
// });
// // console.log("Invoice paid result:", paid, error);
// const latestInvoice = await stripeCli.invoices.retrieve(invoice.id, {
// ...getInvoiceExpansion()
// });
// await InvoiceService.createInvoiceFromStripe({
// sb,
// stripeInvoice: latestInvoice,
// internalCustomerId: customer.internal_id,
// org,
// productIds: [product!.id],
// internalProductIds: [product!.internal_id],
// });
// if (!paid) {
// logger.warn("❗️ Failed to pay for invoice!");
// }
// }
// }
// };
export const handleCreateEntity = async (req: any, res: any) => { export const handleCreateEntity = async (req: any, res: any) => {
try { try {
// Create entity! // Create entity!
const { sb, env, orgId, logtail: logger } = req; const { sb, env, orgId, logtail: logger } = req;
const { customer_id, feature_id, entity: inputEntities } = req.body; const { customer_id } = req.params;
let [customer, features, org] = await Promise.all([ let [customer, features, org] = await Promise.all([
CusService.getByIdOrInternalId({ CusService.getByIdOrInternalId({
@@ -139,6 +245,22 @@ export const handleCreateEntity = async (req: any, res: any) => {
OrgService.getFromReq(req), OrgService.getFromReq(req),
]); ]);
let inputEntities: any[] = [];
if (Array.isArray(req.body)) {
inputEntities = req.body;
} else {
inputEntities = [req.body];
}
let featureIds = [...new Set(inputEntities.map((e: any) => e.feature_id))];
if (featureIds.length > 1) {
throw new RecaseError({
message: "Multiple features not supported",
code: "MULTIPLE_FEATURES_NOT_SUPPORTED",
});
}
let feature_id = featureIds[0];
let feature = features.find((f: any) => f.id === feature_id); let feature = features.find((f: any) => f.id === feature_id);
let cusProducts = await CusService.getFullCusProducts({ let cusProducts = await CusService.getFullCusProducts({
@@ -146,21 +268,25 @@ export const handleCreateEntity = async (req: any, res: any) => {
internalCustomerId: customer.internal_id, internalCustomerId: customer.internal_id,
withProduct: true, withProduct: true,
withPrices: true, withPrices: true,
inStatuses: [CusProductStatus.Active], inStatuses: getActiveCusProductStatuses(),
logger, logger,
}); });
// Fetch existing // Fetch existing
let existingEntities = await EntityService.getInIds({ let existingEntities = await EntityService.get({
sb, sb,
ids: inputEntities.map((e: any) => e.id),
orgId, orgId,
env, env,
internalFeatureId: feature.internal_id, internalFeatureId: feature.internal_id,
internalCustomerId: customer.internal_id,
}); });
console.log("existingEntities", existingEntities.map((e: any) => `${e.id} - ${e.name}, deleted: ${e.deleted}`));
for (const entity of existingEntities) { for (const entity of existingEntities) {
if (entity && !entity.deleted) { if (inputEntities.some((e: any) => e.id === entity.id) && !entity.deleted) {
throw new RecaseError({ throw new RecaseError({
message: `Entity ${entity.id} already exists`, message: `Entity ${entity.id} already exists`,
code: "ENTITY_ALREADY_EXISTS", code: "ENTITY_ALREADY_EXISTS",
@@ -171,6 +297,8 @@ export const handleCreateEntity = async (req: any, res: any) => {
} }
} }
const entityToAction = getEntityToAction({ const entityToAction = getEntityToAction({
inputEntities, inputEntities,
existingEntities, existingEntities,
@@ -179,39 +307,12 @@ export const handleCreateEntity = async (req: any, res: any) => {
cusProducts, cusProducts,
}); });
// 3. CREATE ENTITIES
for (const id in entityToAction) {
let { action, inputEntity, replace } = entityToAction[id];
// Create and add to customer entitlement?
if (action === "create") {
await EntityService.insert({
sb,
data: constructEntity({
inputEntity,
feature,
internalCustomerId: customer.internal_id,
orgId,
env,
}),
});
} else if (action === "replace") {
await EntityService.update({
sb,
internalId: replace.internal_id,
update: {
deleted: false,
},
});
}
}
logger.info(` Created / replaced entities!`);
// 4. CREATE LINKED CUSTOMER ENTITLEMENTS // 3. CREATE LINKED CUSTOMER ENTITLEMENTS
for (const cusProduct of cusProducts) { for (const cusProduct of cusProducts) {
let cusEnts = cusProduct.customer_entitlements; let cusEnts = cusProduct.customer_entitlements;
let product = cusProduct.product; let product = cusProduct.product;
let cusEnt = cusEnts.find( let cusEnt = cusEnts.find(
(e: any) => e.entitlement.feature.id === feature_id (e: any) => e.entitlement.feature.id === feature_id
); );
@@ -225,14 +326,52 @@ export const handleCreateEntity = async (req: any, res: any) => {
(e: any) => e.entitlement.entity_feature_id === feature.id (e: any) => e.entitlement.entity_feature_id === feature.id
); );
// 1. Pay for new seats
let replacedCount = Object.keys(entityToAction).filter(
(id) => entityToAction[id].action === "replace"
).length;
let newCount = Object.keys(entityToAction).filter(
(id) => entityToAction[id].action === "create"
).length;
let { unused } = getCusEntMasterBalance({
cusEnt,
entities: existingEntities,
});
// const originalBalance = cusEnt.balance - (replacedCount || 0) + (unused || 0);
// const newBalance = cusEnt.balance - (newCount + replacedCount) + (unused || 0);
const originalBalance = cusEnt.balance + (unused || 0);
const newBalance = cusEnt.balance - (newCount + replacedCount) + (unused || 0);
// console.log("originalBalance", originalBalance);
// console.log("newBalance", newBalance);
// console.log("Replaced count", replacedCount);
// throw new Error("test");
await adjustAllowance({
sb,
env,
org,
cusPrices: cusProducts.flatMap((p: any) => p.customer_prices),
customer,
affectedFeature: feature,
cusEnt: { ...cusEnt, customer_product: cusProduct },
originalBalance,
newBalance,
deduction: newCount + replacedCount,
product,
replacedCount,
});
await req.pg.query(
`UPDATE customer_entitlements SET balance = balance - $1 WHERE id = $2`,
[newCount, cusEnt.id]
);
// For each linked feature, create customer entitlement for entity... // For each linked feature, create customer entitlement for entity...
for (const linkedCusEnt of linkedCusEnts) { for (const linkedCusEnt of linkedCusEnts) {
// let linkedCusEnt = getLinkedCusEnt({
// linkedFeature,
// cusEnts,
// });
// console.log("linkedCusEnt", linkedCusEnt?.entitlement.feature.id);
let allowance = linkedCusEnt?.entitlement.allowance; let allowance = linkedCusEnt?.entitlement.allowance;
let newEntities = linkedCusEnt?.entities || {}; let newEntities = linkedCusEnt?.entities || {};
@@ -241,13 +380,19 @@ export const handleCreateEntity = async (req: any, res: any) => {
if (entityAction.action === "create") { if (entityAction.action === "create") {
newEntities[entity.id] = { newEntities[entity.id] = {
id: entity.id,
balance: allowance, balance: allowance,
adjustment: 0, adjustment: 0,
}; };
} else if (entityAction.action === "replace") { } else if (entityAction.action === "replace") {
let tmp = newEntities[entityAction.replace.id]; let tmp = newEntities[entityAction.replace.id];
delete newEntities[entityAction.replace.id]; delete newEntities[entityAction.replace.id];
newEntities[entity.id] = tmp; newEntities[entity.id] = {
id: entity.id,
...tmp,
};
} }
} }
@@ -257,36 +402,38 @@ export const handleCreateEntity = async (req: any, res: any) => {
updates: { entities: newEntities }, updates: { entities: newEntities },
}); });
} }
}
// 2. Update main customer entitlement (decrement balance) // 4. CREATE ENTITIES
for (const id in entityToAction) {
let { action, entity, replace } = entityToAction[id];
let replacedCount = Object.keys(entityToAction).filter( // Create and add to customer entitlement?
(id) => entityToAction[id].action === "replace" if (action === "create") {
).length; await EntityService.insert({
let newCount = Object.keys(entityToAction).filter(
(id) => entityToAction[id].action === "create"
).length;
await req.pg.query(
`UPDATE customer_entitlements SET balance = balance - $1 WHERE id = $2`,
[newCount, cusEnt.id]
);
adjustAllowance({
sb, sb,
data: constructEntity({
inputEntity: entity,
feature,
internalCustomerId: customer.internal_id,
orgId,
env, env,
org, }),
cusPrices: cusProducts.flatMap((p: any) => p.customer_prices), });
customer, } else if (action === "replace") {
affectedFeature: feature, await EntityService.update({
cusEnt: { ...cusEnt, customer_product: cusProduct }, sb,
originalBalance: cusEnt.balance, internalId: replace.internal_id,
newBalance: cusEnt.balance - (newCount + replacedCount), update: {
deduction: newCount + replacedCount, id: entity.id,
product, name: entity.name,
replacedCount, deleted: false,
},
}); });
} }
}
logger.info(` Created / replaced entities!`);
res.status(200).json({ res.status(200).json({
success: true, success: true,

View File

@@ -5,28 +5,66 @@ import { ErrCode } from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js"; import { CusService } from "@/internal/customers/CusService.js";
import { adjustAllowance } from "@/trigger/adjustAllowance.js"; import { adjustAllowance } from "@/trigger/adjustAllowance.js";
import { OrgService } from "@/internal/orgs/OrgService.js"; import { OrgService } from "@/internal/orgs/OrgService.js";
import { handleCustomerRaceCondition } from "@/external/redis/redisUtils.js";
import { getCusEntMasterBalance } from "@/internal/customers/entitlements/cusEntUtils.js";
export const handleDeleteEntity = async (req: any, res: any) => { export const handleDeleteEntity = async (req: any, res: any) => {
try { try {
const { orgId, env, logtail: logger, sb } = req; const { orgId, env, logtail: logger, sb } = req;
const entityId = req.params.entity_id; const { customer_id, entity_id } = req.params;
const entity = await EntityService.getById({
await handleCustomerRaceCondition({
action: "entity",
customerId: customer_id,
orgId,
env,
res,
logger,
});
// console.log("Handling race condition for:", customer_id);
// console.log("Customer ID:", customer_id);
// console.log("Entity ID:", entity_id);
const customer = await CusService.getById({
sb: req.sb, sb: req.sb,
entityId, id: customer_id,
orgId: req.orgId,
env: req.env,
logger,
});
if (!customer) {
throw new RecaseError({
message: `Customer ${customer_id} not found`,
code: ErrCode.CustomerNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
const existingEntities = await EntityService.get({
sb: req.sb,
internalCustomerId: customer.internal_id,
orgId: req.orgId, orgId: req.orgId,
env: req.env, env: req.env,
}); });
const entity = existingEntities.find(
(e: any) => e.id === entity_id
);
if (!entity) { if (!entity) {
throw new RecaseError({ throw new RecaseError({
message: `Entity ${entityId} not found`, message: `Entity ${entity_id} not found`,
code: ErrCode.EntityNotFound, code: ErrCode.EntityNotFound,
statusCode: StatusCodes.NOT_FOUND, statusCode: StatusCodes.NOT_FOUND,
}); });
} else if (entity.deleted) { } else if (entity.deleted) {
throw new RecaseError({ throw new RecaseError({
message: `Entity ${entityId} already deleted`, message: `Entity ${entity_id} already deleted`,
code: ErrCode.EntityAlreadyDeleted, code: ErrCode.EntityAlreadyDeleted,
statusCode: StatusCodes.BAD_REQUEST, statusCode: StatusCodes.BAD_REQUEST,
}); });
@@ -40,18 +78,14 @@ export const handleDeleteEntity = async (req: any, res: any) => {
logger, logger,
}); });
const [customer, org] = await Promise.all([ const org = await OrgService.getFromReq(req);
CusService.getByInternalId({
sb: req.sb,
internalId: entity.internal_customer_id,
}),
OrgService.getFromReq(req),
]);
for (const cusProduct of cusProducts) { for (const cusProduct of cusProducts) {
let cusEnts = cusProduct.customer_entitlements; let cusEnts = cusProduct.customer_entitlements;
let product = cusProduct.product; let product = cusProduct.product;
let cusEnt = cusEnts.find( let cusEnt = cusEnts.find(
(e: any) => (e: any) =>
e.entitlement.feature.internal_id === entity.internal_feature_id e.entitlement.feature.internal_id === entity.internal_feature_id
@@ -61,8 +95,14 @@ export const handleDeleteEntity = async (req: any, res: any) => {
continue; continue;
} }
let newBalance = cusEnt.balance + 1; let {unused} = getCusEntMasterBalance({
adjustAllowance({ cusEnt,
entities: existingEntities,
});
let newBalance = (cusEnt.balance + 1) + (unused || 0);
await adjustAllowance({
sb, sb,
env, env,
org, org,
@@ -70,7 +110,7 @@ export const handleDeleteEntity = async (req: any, res: any) => {
customer, customer,
affectedFeature: cusEnt.entitlement.feature, affectedFeature: cusEnt.entitlement.feature,
cusEnt: { ...cusEnt, customer_product: cusProduct }, cusEnt: { ...cusEnt, customer_product: cusProduct },
originalBalance: cusEnt.balance, originalBalance: cusEnt.balance + (unused || 0),
newBalance, newBalance,
deduction: 1, deduction: 1,
product, product,
@@ -85,38 +125,9 @@ export const handleDeleteEntity = async (req: any, res: any) => {
}, },
}); });
// If not X, delete entity AND entitlements... logger.info(` ✅ Finished deleting entity ${entity_id}`);
// await EntityService.update({
// sb: req.sb,
// internalId: entity.internal_id,
// update: {
// deleted: true,
// },
// });
// // console.log("Deleting entity:", entity);
// const customer = await Promise.all([
// CusService.getByInternalId({
// sb: req.sb,
// internalId: entity.internal_customer_id,
// }),
// CusService.getFullCusProducts({
// sb: req.sb,
// internalCustomerId: entity.internal_customer_id,
// withProduct: true,
// withPrices: true,
// logger,
// }),
// ]);
// if (!customer) {
// throw new RecaseError({
// message: `Customer ${entity.internal_customer_id} not found`,
// code: ErrCode.CustomerNotFound,
// statusCode: StatusCodes.NOT_FOUND,
// });
// }
res.status(200).json({ res.status(200).json({
success: true, success: true,

View File

@@ -6,6 +6,7 @@ import {
CusProductStatus, CusProductStatus,
EntitlementWithFeature, EntitlementWithFeature,
FeatureOptions, FeatureOptions,
FeatureType,
FullCustomerEntitlement, FullCustomerEntitlement,
Organization, Organization,
Price, Price,
@@ -151,6 +152,44 @@ export const updateOneTimeCusProduct = async ({
} }
} }
// Handle adding quantity to base entitlements if cus product purchased multiple times.
for (const entitlement of attachParams.entitlements) {
const relatedPrice = getEntRelatedPrice(entitlement, attachParams.prices);
const feature = entitlement.feature;
if (relatedPrice || feature.type == FeatureType.Boolean || entitlement.allowance_type === AllowanceType.Unlimited) {
continue;
}
const newOptionIndex = newOptionsList.findIndex(
(o) => o.internal_feature_id === entitlement.internal_feature_id
);
if (newOptionIndex === -1) {
// Get existing option
const existingOption = existingCusProduct.options.find(
(o) => o.internal_feature_id === entitlement.internal_feature_id
);
if (existingOption) {
newOptionsList.push({
feature_id: entitlement.feature.id,
quantity: (existingOption?.quantity || 0) + 1,
internal_feature_id: entitlement.internal_feature_id,
});
} else {
newOptionsList.push({
feature_id: entitlement.feature.id,
quantity: 2,
internal_feature_id: entitlement.internal_feature_id,
});
}
}
}
await CusProductService.update({ await CusProductService.update({
sb, sb,
cusProductId: existingCusProduct.id, cusProductId: existingCusProduct.id,

View File

@@ -214,8 +214,9 @@ const handleOneOffPrices = async ({
const stripeCli = createStripeCli({ org, env: customer.env }); const stripeCli = createStripeCli({ org, env: customer.env });
logger.info(" 1. Creating invoice"); logger.info(" 1. Creating invoice");
const stripeInvoice = await stripeCli.invoices.create({ let stripeInvoice = await stripeCli.invoices.create({
customer: customer.processor.id, customer: customer.processor.id,
auto_advance: false,
}); });
// 2. Create invoice items // 2. Create invoice items
@@ -245,7 +246,7 @@ const handleOneOffPrices = async ({
} }
if (!attachParams.invoiceOnly) { if (!attachParams.invoiceOnly) {
const finalizedInvoice = await stripeCli.invoices.finalizeInvoice( stripeInvoice = await stripeCli.invoices.finalizeInvoice(
stripeInvoice.id, stripeInvoice.id,
getInvoiceExpansion() getInvoiceExpansion()
); );
@@ -259,19 +260,20 @@ const handleOneOffPrices = async ({
logger, logger,
}); });
if (!paid && fromRequest) { if (!paid) {
await stripeCli.invoices.voidInvoice(stripeInvoice.id); await stripeCli.invoices.voidInvoice(stripeInvoice.id);
if (fromRequest && org.config.checkout_on_failed_payment) {
await handleCreateCheckout({ await handleCreateCheckout({
sb, sb,
req, req,
res, res,
attachParams, attachParams,
}); });
return; } else {
} else if (!paid) {
throw error; throw error;
} }
} }
}
// Insert full customer product // Insert full customer product
logger.info(" 3. Creating full customer product"); logger.info(" 3. Creating full customer product");

View File

@@ -157,6 +157,7 @@ export const cancelFutureProductSchedule = async ({
entitlements: fullCurProduct.entitlements, entitlements: fullCurProduct.entitlements,
freeTrial: null, freeTrial: null,
optionsList: [], optionsList: [],
entities: [],
}, },
}); });

View File

@@ -351,13 +351,15 @@ export const getCusBalancesByEntitlement = async ({
data[key].balance += balance || 0; data[key].balance += balance || 0;
data[key].adjustment += adjustment || 0; data[key].adjustment += adjustment || 0;
data[key].total += let total = (getResetBalance({
(getResetBalance({
entitlement: ent, entitlement: ent,
options: getEntOptions(cusProduct.options, ent), options: getEntOptions(cusProduct.options, ent),
relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price, relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price,
}) || 0) * count; }) || 0) * count;
data[key].total += total;
data[key].unused += unused || 0; data[key].unused += unused || 0;
} }
@@ -684,9 +686,11 @@ export const getTotalNegativeBalance = ({
export const getExistingUsageFromCusProducts = ({ export const getExistingUsageFromCusProducts = ({
entitlement, entitlement,
cusProducts, cusProducts,
entities,
}: { }: {
entitlement: EntitlementWithFeature; entitlement: EntitlementWithFeature;
cusProducts?: FullCusProduct[]; cusProducts?: FullCusProduct[];
entities: Entity[];
}) => { }) => {
if (!entitlement || entitlement.feature.type === FeatureType.Boolean) { if (!entitlement || entitlement.feature.type === FeatureType.Boolean) {
return 0; return 0;
@@ -715,5 +719,16 @@ export const getExistingUsageFromCusProducts = ({
// // Calculate existing usage // // Calculate existing usage
let existingAllowance = existingCusEnt.entitlement.allowance!; let existingAllowance = existingCusEnt.entitlement.allowance!;
return existingAllowance - existingCusEnt.balance!;
let { balance, adjustment, count, unused } = getCusEntMasterBalance({
cusEnt: existingCusEnt as any,
entities: entities,
});
existingUsage = existingAllowance - balance!;
if (unused && unused > 0) {
existingUsage -= unused;
}
return existingUsage;
}; };

View File

@@ -1,6 +1,7 @@
import { import {
Customer, Customer,
EntitlementWithFeature, EntitlementWithFeature,
Entity,
FeatureOptions, FeatureOptions,
FreeTrial, FreeTrial,
FullCusProduct, FullCusProduct,
@@ -33,6 +34,8 @@ export type AttachParams = {
invoiceOnly?: boolean | undefined; invoiceOnly?: boolean | undefined;
billingAnchor?: number | undefined; billingAnchor?: number | undefined;
metadata?: Record<string, string> | undefined; metadata?: Record<string, string> | undefined;
entities: Entity[];
}; };
export type InsertCusProductParams = { export type InsertCusProductParams = {

View File

@@ -33,6 +33,7 @@ import { createNewCustomer } from "@/internal/api/customers/handlers/handleCreat
import { CusService } from "../CusService.js"; import { CusService } from "../CusService.js";
import { getExistingCusProducts } from "../add-product/handleExistingProduct.js"; import { getExistingCusProducts } from "../add-product/handleExistingProduct.js";
import { getPricesForCusProduct } from "../change-product/scheduleUtils.js"; import { getPricesForCusProduct } from "../change-product/scheduleUtils.js";
import { EntityService } from "@/internal/api/entities/EntityService.js";
const getOrCreateCustomerAndProducts = async ({ const getOrCreateCustomerAndProducts = async ({
sb, sb,
@@ -309,6 +310,13 @@ export const getFullCusProductData = async ({
logger, logger,
}); });
const entities = await EntityService.get({
sb,
internalCustomerId: customer.internal_id,
orgId,
env,
});
let newOptionsList: FeatureOptions[] = []; let newOptionsList: FeatureOptions[] = [];
for (const options of optionsListInput) { for (const options of optionsListInput) {
@@ -356,6 +364,7 @@ export const getFullCusProductData = async ({
.flat() as EntitlementWithFeature[], .flat() as EntitlementWithFeature[],
freeTrial, freeTrial,
cusProducts, cusProducts,
entities,
}; };
} }
@@ -441,5 +450,6 @@ export const getFullCusProductData = async ({
entitlements: entitlementsWithFeature as EntitlementWithFeature[], entitlements: entitlementsWithFeature as EntitlementWithFeature[],
freeTrial: uniqueFreeTrial, freeTrial: uniqueFreeTrial,
cusProducts, cusProducts,
entities,
}; };
}; };

View File

@@ -317,6 +317,7 @@ export const processFullCusProduct = ({
}) => { }) => {
// Process prices // Process prices
const prices = cusProduct.customer_prices.map((cp) => { const prices = cusProduct.customer_prices.map((cp) => {
let price = cp.price; let price = cp.price;
@@ -376,6 +377,8 @@ export const processFullCusProduct = ({
} }
} }
}); });
const trialing = const trialing =
cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now(); cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
@@ -392,6 +395,9 @@ export const processFullCusProduct = ({
current_period_end: baseSub?.current_period_end current_period_end: baseSub?.current_period_end
? baseSub.current_period_end * 1000 ? baseSub.current_period_end * 1000
: null, : null,
current_period_start: baseSub?.current_period_start
? baseSub.current_period_start * 1000
: null,
}; };
} }

View File

@@ -279,10 +279,11 @@ export const getPriceAmount = ({
options?: FeatureOptions; options?: FeatureOptions;
relatedEnt?: EntitlementWithFeature; relatedEnt?: EntitlementWithFeature;
}) => { }) => {
if (price.billing_type == BillingType.OneOff) { let billingType = getBillingType(price.config!);
if (billingType == BillingType.OneOff) {
let config = price.config as FixedPriceConfig; let config = price.config as FixedPriceConfig;
return Number(config.amount.toFixed(2)); return Number(config.amount.toFixed(2));
} else if (price.billing_type == BillingType.UsageInAdvance) { } else if (billingType == BillingType.UsageInAdvance) {
let quantity = options?.quantity!; let quantity = options?.quantity!;
let config = price.config as UsagePriceConfig; let config = price.config as UsagePriceConfig;

View File

@@ -81,6 +81,7 @@ const publicRouterMiddleware = async (req: any, res: any, next: any) => {
publicRouter.use(publicRouterMiddleware); publicRouter.use(publicRouterMiddleware);
publicRouter.get("/customers/:customer_id", async (req: any, res: any) => { publicRouter.get("/customers/:customer_id", async (req: any, res: any) => {
try { try {
const customerId = req.params.customer_id; const customerId = req.params.customer_id;
console.log("Getting customer (public)", customerId); console.log("Getting customer (public)", customerId);
@@ -126,16 +127,33 @@ publicRouter.get("/customers/:customer_id", async (req: any, res: any) => {
publicRouter.get( publicRouter.get(
"/customers/:customerId/products", "/customers/:customerId/products",
async (req: any, res: any) => { async (req: any, res: any) => {
try {
const customerId = req.params.customerId; const customerId = req.params.customerId;
const cusProducts = await CusProductService.getFullByCustomerId({ const customer = await CusService.getById({
sb: req.sb, sb: req.sb,
customerId, id: customerId,
orgId: req.org.id, orgId: req.org.id,
env: req.env, env: req.env,
inStatuses: [CusProductStatus.Active, CusProductStatus.Scheduled], logger: req.logtail,
}); });
if (!customer) {
return res.status(404).json({
message: `Customer ${customerId} not found`,
});
}
const cusProducts = await CusService.getFullCusProducts({
sb: req.sb,
internalCustomerId: customer.internal_id,
inStatuses: [CusProductStatus.Active, CusProductStatus.Scheduled],
withProduct: true,
withPrices: true,
});
if (!cusProducts || cusProducts.length === 0) { if (!cusProducts || cusProducts.length === 0) {
return res.status(200).json({ return res.status(200).json({
main: [], main: [],
@@ -147,7 +165,12 @@ publicRouter.get(
let addOns = []; let addOns = [];
for (const cusProduct of cusProducts) { for (const cusProduct of cusProducts) {
let processed = processFullCusProduct(cusProduct);
let processed = processFullCusProduct({
cusProduct,
org: req.org,
subs: [],
});
if (processed.status == CusProductStatus.Trialing) { if (processed.status == CusProductStatus.Trialing) {
processed.status = CusProductStatus.Active; processed.status = CusProductStatus.Active;
@@ -161,18 +184,21 @@ publicRouter.get(
} }
} }
// console.log("main", main);
res.status(200).json({ res.status(200).json({
main, main,
add_ons: addOns, add_ons: addOns,
}); });
} catch (error) {
handleRequestError({ req, error, res, action: "get customer products" });
}
} }
); );
publicRouter.get( publicRouter.get(
"/products/:product_id/options", "/products/:product_id/options",
async (req: any, res: any) => { async (req: any, res: any) => {
try {
const product = await ProductService.getFullProductStrict({ const product = await ProductService.getFullProductStrict({
sb: req.sb, sb: req.sb,
productId: req.params.product_id, productId: req.params.product_id,
@@ -191,6 +217,9 @@ publicRouter.get(
const options = getOptionsFromPrices(prices, features); const options = getOptionsFromPrices(prices, features);
res.status(200).json(options); res.status(200).json(options);
} catch (error) {
handleRequestError({ req, error, res, action: "get product options" });
}
} }
); );

View File

@@ -1,4 +1,5 @@
import { import {
ErrCode,
FullCusProduct, FullCusProduct,
FullCustomerEntitlement, FullCustomerEntitlement,
Product, Product,
@@ -31,9 +32,11 @@ import { generateId } from "@/utils/genUtils.js";
import { createStripeInvoiceItem } from "@/internal/customers/invoices/invoiceItemUtils.js"; import { createStripeInvoiceItem } from "@/internal/customers/invoices/invoiceItemUtils.js";
import { createLogtailWithContext } from "@/external/logtail/logtailUtils.js"; import { createLogtailWithContext } from "@/external/logtail/logtailUtils.js";
import { LoggerAction } from "@autumn/shared"; import { LoggerAction } from "@autumn/shared";
import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js"; import { getInvoiceExpansion, payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js";
import { isTrialing } from "@/internal/customers/products/cusProductUtils.js"; import { isTrialing } from "@/internal/customers/products/cusProductUtils.js";
import { ProductService } from "@/internal/products/ProductService.js"; import { ProductService } from "@/internal/products/ProductService.js";
import { InvoiceService } from "@/internal/customers/invoices/InvoiceService.js";
import RecaseError from "@/utils/errorUtils.js";
type CusEntWithCusProduct = FullCustomerEntitlement & { type CusEntWithCusProduct = FullCustomerEntitlement & {
customer_product: CusProduct; customer_product: CusProduct;
@@ -321,8 +324,9 @@ export const adjustAllowance = async ({
return; return;
} }
let quantity = newUsage + cusEnt.entitlement.allowance!;
let quantity = newUsage + cusEnt.entitlement.allowance!;
let prorationBehaviour = "create_prorations"; let prorationBehaviour = "create_prorations";
// If prorate unused is false, then remove end of cycle // If prorate unused is false, then remove end of cycle
@@ -330,12 +334,10 @@ export const adjustAllowance = async ({
prorationBehaviour = "none"; prorationBehaviour = "none";
const downgrade = quantity < (subItem.quantity || 0); const downgrade = quantity < (subItem.quantity || 0);
if (!downgrade && !isTrialing(cusProduct as FullCusProduct)) { if (!downgrade && !isTrialing(cusProduct as FullCusProduct)) {
let entitlement = cusEnt.entitlement; let entitlement = cusEnt.entitlement;
let newUsage = entitlement.allowance! - newBalance; let newUsage = entitlement.allowance! - newBalance;
let oldUsage = entitlement.allowance! - originalBalance; let oldUsage = entitlement.allowance! - originalBalance + (replacedCount || 0);
newUsage = newUsage - (replacedCount || 0);
let newAmount = getPriceForOverage(cusPrice.price, newUsage); let newAmount = getPriceForOverage(cusPrice.price, newUsage);
let oldAmount = getPriceForOverage(cusPrice.price, oldUsage); let oldAmount = getPriceForOverage(cusPrice.price, oldUsage);
@@ -379,6 +381,7 @@ export const adjustAllowance = async ({
}, },
}); });
const { paid, error } = await payForInvoice({ const { paid, error } = await payForInvoice({
fullOrg: org, fullOrg: org,
env, env,
@@ -387,6 +390,27 @@ export const adjustAllowance = async ({
logger, logger,
}); });
if (!paid) {
await stripeCli.invoices.voidInvoice(invoice.id);
throw new RecaseError({
message: "Failed to pay for invoice",
code: ErrCode.PayInvoiceFailed,
})
}
const latestInvoice = await stripeCli.invoices.retrieve(invoice.id, {
...getInvoiceExpansion()
});
await InvoiceService.createInvoiceFromStripe({
sb,
stripeInvoice: latestInvoice,
internalCustomerId: customer.internal_id,
org,
productIds: [product!.id],
internalProductIds: [product!.internal_id],
});
if (!paid) { if (!paid) {
logger.warn("❗️ Failed to pay for invoice!"); logger.warn("❗️ Failed to pay for invoice!");
} }
@@ -394,6 +418,8 @@ export const adjustAllowance = async ({
} }
} }
if (quantity < 0) { if (quantity < 0) {
quantity = 0; quantity = 0;
logger.warn("❗️ Warning: quantity is negative, setting to 0"); logger.warn("❗️ Warning: quantity is negative, setting to 0");

View File

@@ -186,12 +186,14 @@ export const performDeductionOnCusEnt = ({
entityId, entityId,
allowNegativeBalance = false, allowNegativeBalance = false,
addAdjustment = false, addAdjustment = false,
setZeroAdjustment = false,
}: { }: {
cusEnt: FullCustomerEntitlement; cusEnt: FullCustomerEntitlement;
toDeduct: number; toDeduct: number;
entityId?: string | null; entityId?: string | null;
allowNegativeBalance?: boolean; allowNegativeBalance?: boolean;
addAdjustment?: boolean; addAdjustment?: boolean;
setZeroAdjustment?: boolean;
}) => { }) => {
let newEntities = structuredClone(cusEnt.entities); let newEntities = structuredClone(cusEnt.entities);
let newBalance = structuredClone(cusEnt.balance); let newBalance = structuredClone(cusEnt.balance);
@@ -229,6 +231,10 @@ export const performDeductionOnCusEnt = ({
newEntities![entityId!]!.adjustment = adjustment - newDeducted!; newEntities![entityId!]!.adjustment = adjustment - newDeducted!;
} }
if (setZeroAdjustment) {
newEntities![entityId!]!.adjustment = 0;
}
toDeductCursor = newToDeduct!; toDeductCursor = newToDeduct!;
deducted += newDeducted!; deducted += newDeducted!;
} }
@@ -255,6 +261,10 @@ export const performDeductionOnCusEnt = ({
newEntities![entityId!]!.adjustment = adjustment - newDeducted!; newEntities![entityId!]!.adjustment = adjustment - newDeducted!;
} }
if (setZeroAdjustment) {
newEntities![entityId!]!.adjustment = 0;
}
toDeduct = newToDeduct!; toDeduct = newToDeduct!;
deducted += newDeducted!; deducted += newDeducted!;
} }
@@ -294,6 +304,7 @@ export const deductAllowanceFromCusEnt = async ({
featureDeductions, featureDeductions,
willDeductCredits = false, willDeductCredits = false,
entityId, entityId,
setZeroAdjustment = false,
}: { }: {
toDeduct: number; toDeduct: number;
deductParams: DeductParams; deductParams: DeductParams;
@@ -302,6 +313,7 @@ export const deductAllowanceFromCusEnt = async ({
featureDeductions: any; featureDeductions: any;
willDeductCredits?: boolean; willDeductCredits?: boolean;
entityId?: string | null; entityId?: string | null;
setZeroAdjustment?: boolean;
}) => { }) => {
const { sb, feature, env, org, cusPrices, customer, properties } = const { sb, feature, env, org, cusPrices, customer, properties } =
deductParams; deductParams;
@@ -328,6 +340,7 @@ export const deductAllowanceFromCusEnt = async ({
toDeduct, toDeduct,
entityId, entityId,
allowNegativeBalance: false, allowNegativeBalance: false,
setZeroAdjustment,
}); });
let originalGrpBalance = getTotalNegativeBalance({ let originalGrpBalance = getTotalNegativeBalance({
@@ -346,13 +359,17 @@ export const deductAllowanceFromCusEnt = async ({
// entities: newEntities, // entities: newEntities,
// }); // });
let updates: any = {
balance: newBalance,
entities: newEntities,
}
if (setZeroAdjustment) {
updates.adjustment = 0;
}
await CustomerEntitlementService.update({ await CustomerEntitlementService.update({
sb, sb,
id: cusEnt.id, id: cusEnt.id,
updates: { updates,
balance: newBalance,
entities: newEntities,
},
}); });
await adjustAllowance({ await adjustAllowance({
@@ -405,11 +422,13 @@ export const deductFromUsageBasedCusEnt = async ({
deductParams, deductParams,
cusEnts, cusEnts,
entityId, entityId,
setZeroAdjustment = false,
}: { }: {
toDeduct: number; toDeduct: number;
deductParams: DeductParams; deductParams: DeductParams;
cusEnts: FullCustomerEntitlement[]; cusEnts: FullCustomerEntitlement[];
entityId?: string | null; entityId?: string | null;
setZeroAdjustment?: boolean;
}) => { }) => {
const { sb, feature, env, org, cusPrices, customer, properties } = const { sb, feature, env, org, cusPrices, customer, properties } =
deductParams; deductParams;
@@ -452,6 +471,7 @@ export const deductFromUsageBasedCusEnt = async ({
toDeduct, toDeduct,
entityId, entityId,
allowNegativeBalance: true, allowNegativeBalance: true,
setZeroAdjustment,
}); });
// console.log("NEW BALANCE", newBalance); // console.log("NEW BALANCE", newBalance);
@@ -469,13 +489,18 @@ export const deductFromUsageBasedCusEnt = async ({
entities: newEntities!, entities: newEntities!,
}); });
let updates: any = {
balance: newBalance,
entities: newEntities,
}
if (setZeroAdjustment) {
updates.adjustment = 0;
}
await CustomerEntitlementService.update({ await CustomerEntitlementService.update({
sb, sb,
id: usageBasedEnt.id, id: usageBasedEnt.id,
updates: { updates
balance: newBalance,
entities: newEntities,
},
}); });
// const totalNegativeBalance = getTotalNegativeBalance(usageBasedEnt); // const totalNegativeBalance = getTotalNegativeBalance(usageBasedEnt);

View File

@@ -220,13 +220,13 @@ export const updateUsage = async ({
setUsage, setUsage,
}); });
// 2. Handle group_by initialization // // 2. Handle group_by initialization
await initGroupBalancesForEvent({ // await initGroupBalancesForEvent({
sb, // sb,
features, // features,
cusEnts, // cusEnts,
properties, // properties,
}); // });
// 3. Return if no customer entitlements or features found // 3. Return if no customer entitlements or features found
if (cusEnts.length === 0 || features.length === 0) { if (cusEnts.length === 0 || features.length === 0) {
@@ -258,6 +258,7 @@ export const updateUsage = async ({
}, },
featureDeductions, featureDeductions,
willDeductCredits: true, willDeductCredits: true,
setZeroAdjustment: true,
}); });
} }
@@ -277,6 +278,7 @@ export const updateUsage = async ({
customer, customer,
properties, properties,
}, },
setZeroAdjustment: true,
}); });
} }

View File

@@ -1 +1,8 @@
import { CusProductStatus } from "@autumn/shared";
export const BREAK_API_VERSION = 0.2; export const BREAK_API_VERSION = 0.2;
export const getActiveCusProductStatuses = () => [
CusProductStatus.Active,
CusProductStatus.PastDue,
];

View File

@@ -9,10 +9,11 @@ if [ "$1" == "basic-parallel" ]; then
tests/attach/**/*.ts \ tests/attach/**/*.ts \
elif [ "$1" == "advanced-parallel" ]; then elif [ "$1" == "advanced-parallel" ]; then
MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \ MOCHA_PARALLEL=true \
'tests/advanced/usage/*.ts' \
&& $MOCHA_CMD 'tests/advanced/coupons/*.ts' \ && $MOCHA_CMD 'tests/advanced/coupons/*.ts' \
&& $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' # $MOCHA_SETUP \
# && $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' \
# && $MOCHA_CMD 'tests/advanced/usage/*.ts' \
elif [ "$1" == "alex-parallel" ]; then elif [ "$1" == "alex-parallel" ]; then

View File

@@ -100,7 +100,7 @@ export const ErrCode = {
GetCusPriceFailed: "get_cus_price_failed", GetCusPriceFailed: "get_cus_price_failed",
// Pay for invoice // Pay for invoice
PayInvoiceFailed: "pay_invoice_failed", PayInvoiceFailed: "invoice_payment_failed",
// COUPONS // COUPONS
PromoCodeAlreadyExistsInStripe: "promo_code_already_exists_in_stripe", PromoCodeAlreadyExistsInStripe: "promo_code_already_exists_in_stripe",

View File

@@ -8,6 +8,7 @@ export const OrgConfigSchema = z.object({
prorate_unused: z.boolean().default(true), prorate_unused: z.boolean().default(true),
api_version: z.number().default(0.2), api_version: z.number().default(0.2),
checkout_on_failed_payment: z.boolean().default(true),
}); });
export type OrgConfig = z.infer<typeof OrgConfigSchema>; export type OrgConfig = z.infer<typeof OrgConfigSchema>;