fix: updated create and remove entity
This commit is contained in:
13
package-lock.json
generated
13
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
52
server/src/external/redis/redisUtils.ts
vendored
52
server/src/external/redis/redisUtils.ts
vendored
@@ -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,
|
||||||
|
|||||||
@@ -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 ||
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -174,6 +174,7 @@ export const createNewCustomer = async ({
|
|||||||
optionsList: [],
|
optionsList: [],
|
||||||
cusProducts: [],
|
cusProducts: [],
|
||||||
invoiceOnly: true,
|
invoiceOnly: true,
|
||||||
|
entities: [],
|
||||||
},
|
},
|
||||||
fromRequest: false,
|
fromRequest: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export const cancelFutureProductSchedule = async ({
|
|||||||
entitlements: fullCurProduct.entitlements,
|
entitlements: fullCurProduct.entitlements,
|
||||||
freeTrial: null,
|
freeTrial: null,
|
||||||
optionsList: [],
|
optionsList: [],
|
||||||
|
entities: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
];
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
Reference in New Issue
Block a user