conflict resolution
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import { Feature } from "@autumn/shared";
|
||||
import {
|
||||
Entitlement,
|
||||
Feature,
|
||||
Price,
|
||||
PriceType,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
|
||||
export const getFeature = (
|
||||
internalFeatureId: string | undefined,
|
||||
|
||||
@@ -31,11 +31,13 @@ function CreateUsagePrice({
|
||||
setConfig,
|
||||
usageTiers,
|
||||
setUsageTiers,
|
||||
price,
|
||||
}: {
|
||||
config: any;
|
||||
setConfig: (config: any) => void;
|
||||
usageTiers: any[];
|
||||
setUsageTiers: (usageTiers: any[]) => void;
|
||||
price: Price;
|
||||
}) {
|
||||
const { features, product } = useProductContext();
|
||||
|
||||
@@ -65,6 +67,13 @@ function CreateUsagePrice({
|
||||
|
||||
const filteredEntitlements = product.entitlements.filter(
|
||||
(entitlement: EntitlementWithFeature) => {
|
||||
const config = price.config as UsagePriceConfig;
|
||||
if (
|
||||
config &&
|
||||
config.internal_feature_id == entitlement.internal_feature_id
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
product.prices.some((price: Price) => {
|
||||
const config = price.config as UsagePriceConfig;
|
||||
@@ -93,6 +102,7 @@ function CreateUsagePrice({
|
||||
?.internal_id,
|
||||
});
|
||||
}}
|
||||
disabled={!!(price.config as UsagePriceConfig)?.internal_feature_id}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select entitlement" />
|
||||
|
||||
@@ -93,6 +93,7 @@ export const PricingConfig = ({
|
||||
setConfig={setUsageConfig}
|
||||
usageTiers={usageTiers}
|
||||
setUsageTiers={setUsageTiers}
|
||||
price={price}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -35,13 +35,6 @@ const resetCustomerEntitlement = async ({
|
||||
cusEnt: FullCustomerEntitlementWithProduct;
|
||||
}) => {
|
||||
try {
|
||||
// console.log(`Resetting cusEnt ${cusEnt.id}`);
|
||||
// console.log(
|
||||
// `Customer: ${chalk.yellowBright(
|
||||
// cusEnt.customer_id
|
||||
// )}, Feature: ${chalk.yellowBright(cusEnt.entitlement.feature_id)}`
|
||||
// );
|
||||
|
||||
// 1. Get allowance and quantity
|
||||
const allowance = cusEnt.entitlement.allowance || 0;
|
||||
|
||||
@@ -54,12 +47,6 @@ const resetCustomerEntitlement = async ({
|
||||
let quantity = (entOptions && entOptions.quantity) || 1;
|
||||
const newBalance = allowance * quantity;
|
||||
|
||||
// console.log(
|
||||
// `Allowance: ${chalk.yellow(allowance)} | Quantity: ${chalk.yellow(
|
||||
// quantity
|
||||
// )} | New Balance: ${chalk.yellow(newBalance)}`
|
||||
// );
|
||||
|
||||
// 3. Update the next_reset_at for each entitlement
|
||||
const nextResetAt = getNextResetAt(
|
||||
new Date(cusEnt.next_reset_at!),
|
||||
|
||||
16
server/src/external/stripe/stripeMeterUtils.ts
vendored
Normal file
16
server/src/external/stripe/stripeMeterUtils.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
export const sendMeterEvent = async ({
|
||||
sb,
|
||||
customerId,
|
||||
event,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
customerId: string;
|
||||
event: Event;
|
||||
}) => {
|
||||
const stripeCli = createStripeCli({
|
||||
orgId: org.id,
|
||||
env: org.env,
|
||||
});
|
||||
};
|
||||
@@ -23,6 +23,8 @@ export const createWebhookEndpoint = async (
|
||||
"customer.subscription.deleted",
|
||||
"checkout.session.completed",
|
||||
"invoice.paid",
|
||||
"invoice.created",
|
||||
"invoice.finalized",
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
41
server/src/external/stripe/stripePriceUtils.ts
vendored
41
server/src/external/stripe/stripePriceUtils.ts
vendored
@@ -8,11 +8,16 @@ import {
|
||||
Price,
|
||||
UsagePriceConfig,
|
||||
FeatureOptions,
|
||||
Entitlement,
|
||||
Feature,
|
||||
Product,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { billingIntervalToStripe } from "./utils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
import Stripe from "stripe";
|
||||
import { priceToStripeTiers } from "@/internal/prices/priceUtils.js";
|
||||
|
||||
export const priceToStripeItem = ({
|
||||
price,
|
||||
@@ -114,3 +119,39 @@ export const priceToStripeItem = ({
|
||||
lineItemMeta,
|
||||
};
|
||||
};
|
||||
|
||||
export const createStripeMeteredPrice = async ({
|
||||
stripeCli,
|
||||
meterId,
|
||||
product,
|
||||
price,
|
||||
entitlements,
|
||||
feature,
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
meterId: string;
|
||||
product: Product;
|
||||
price: Price;
|
||||
entitlements: Entitlement[];
|
||||
feature: Feature;
|
||||
}) => {
|
||||
return await stripeCli.prices.create({
|
||||
// product: product.processor!.id,
|
||||
product_data: {
|
||||
name: `${product.name} - ${feature!.name}`,
|
||||
},
|
||||
// unit_amount: ,
|
||||
billing_scheme: "tiered",
|
||||
tiers_mode: "volume",
|
||||
tiers: priceToStripeTiers(
|
||||
price,
|
||||
entitlements.find((e) => e.internal_feature_id === feature!.internal_id)!
|
||||
),
|
||||
currency: "usd",
|
||||
recurring: {
|
||||
...(billingIntervalToStripe(price.config!.interval!) as any),
|
||||
meter: meterId,
|
||||
usage_type: "metered",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
14
server/src/external/stripe/stripeWebhooks.ts
vendored
14
server/src/external/stripe/stripeWebhooks.ts
vendored
@@ -9,7 +9,8 @@ import { handleSubCreated } from "./webhookHandlers/handleSubCreated.js";
|
||||
import { getStripeWebhookSecret } from "@/internal/orgs/orgUtils.js";
|
||||
import { handleInvoicePaid } from "./webhookHandlers/handleInvoicePaid.js";
|
||||
import chalk from "chalk";
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { handleInvoiceCreated } from "./webhookHandlers/handleInvoiceCreated.js";
|
||||
|
||||
export const stripeWebhookRouter = express.Router();
|
||||
|
||||
@@ -108,6 +109,17 @@ stripeWebhookRouter.post(
|
||||
req: request,
|
||||
});
|
||||
break;
|
||||
|
||||
case "invoice.created":
|
||||
const createdInvoice = event.data.object;
|
||||
await handleInvoiceCreated({
|
||||
sb: request.sb,
|
||||
org,
|
||||
invoice: createdInvoice,
|
||||
env,
|
||||
event,
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
handleRequestError({
|
||||
|
||||
90
server/src/external/stripe/webhookHandlers/handleInvoiceCreated.ts
vendored
Normal file
90
server/src/external/stripe/webhookHandlers/handleInvoiceCreated.ts
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
import { CustomerEntitlementService } from "@/internal/customers/entitlements/CusEntitlementService.js";
|
||||
import { CusProductService } from "@/internal/customers/products/CusProductService.js";
|
||||
import { getBillingType } from "@/internal/prices/priceUtils.js";
|
||||
import {
|
||||
AppEnv,
|
||||
BillingType,
|
||||
Organization,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import Stripe from "stripe";
|
||||
import { createStripeCli } from "../utils.js";
|
||||
|
||||
export const handleInvoiceCreated = async ({
|
||||
sb,
|
||||
org,
|
||||
invoice,
|
||||
env,
|
||||
event,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
org: Organization;
|
||||
invoice: Stripe.Invoice;
|
||||
env: AppEnv;
|
||||
event: Stripe.Event;
|
||||
}) => {
|
||||
console.log("Invoice created: ", invoice.id);
|
||||
// Get stripe subscriptions
|
||||
if (invoice.subscription) {
|
||||
const activeProducts = await CusProductService.getActiveByStripeSubId({
|
||||
sb,
|
||||
stripeSubId: invoice.subscription as string,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
if (activeProducts.length != 1) {
|
||||
console.log("Invalid number of active products: ", activeProducts.length);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeProduct = activeProducts[0];
|
||||
|
||||
// 1. Remove invoiceItem from stripe
|
||||
// Get cus ents
|
||||
const cusProductWithEntsAndPrices =
|
||||
await CusProductService.getEntsAndPrices({
|
||||
sb,
|
||||
cusProductId: activeProduct.id,
|
||||
});
|
||||
|
||||
const cusEnts = cusProductWithEntsAndPrices.customer_entitlements;
|
||||
const cusPrices = cusProductWithEntsAndPrices.customer_prices;
|
||||
|
||||
const cusUsagePrice = cusPrices.find(
|
||||
(cusPrice: any) =>
|
||||
getBillingType(cusPrice.price.config.type) === BillingType.UsageInArrear
|
||||
);
|
||||
|
||||
if (cusUsagePrice) {
|
||||
const config = cusUsagePrice.price.config as UsagePriceConfig;
|
||||
console.log("Cus usage price config:", config);
|
||||
// Remove price from stripe
|
||||
// console.log("Cus usage price:", cusUsagePrice);
|
||||
// Get invoice items
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
// console.log("Invoice items:", invoice.lines.data);
|
||||
for (const item of invoice.lines.data) {
|
||||
if (item.price?.id == config.stripe_price_id) {
|
||||
// console.log("Removing invoice item:", item.id, item.price?.id);
|
||||
}
|
||||
|
||||
// await stripeCli.invoiceItems.del(item.id);
|
||||
await stripeCli.invoiceItems.update(item.id, {
|
||||
// amount: 1000,
|
||||
description: "Test update",
|
||||
});
|
||||
}
|
||||
|
||||
// // Add invoice item
|
||||
// await stripeCli.invoiceItems.create({
|
||||
// invoice: invoice.id,
|
||||
// amount: 100,
|
||||
// currency: org.default_currency,
|
||||
// customer: invoice.customer as string,
|
||||
// description: `Usage for ${config.stripe_price_id}`,
|
||||
// });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CusProductStatus,
|
||||
Customer,
|
||||
CustomerResponseSchema,
|
||||
FullCustomerPrice,
|
||||
} from "@autumn/shared";
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
@@ -16,13 +17,15 @@ import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { EventService } from "../events/EventService.js";
|
||||
import { CustomerEntitlementService } from "@/internal/customers/entitlements/CusEntitlementService.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { createNewCustomer } from "./cusUtils.js";
|
||||
import { createNewCustomer, getCusEntsAndPrices } from "./cusUtils.js";
|
||||
import { CusProductService } from "@/internal/customers/products/CusProductService.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import {
|
||||
getCusBalancesByEntitlement,
|
||||
getCusBalancesByProduct,
|
||||
getRelatedCusPrice,
|
||||
sortCusEntsForDeduction,
|
||||
updateCusEntInStripe,
|
||||
} from "@/internal/customers/entitlements/cusEntUtils.js";
|
||||
import { processFullCusProduct } from "@/internal/customers/products/cusProductUtils.js";
|
||||
import {
|
||||
@@ -30,6 +33,8 @@ import {
|
||||
processInvoice,
|
||||
} from "@/internal/customers/invoices/InvoiceService.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { CusPriceService } from "@/internal/customers/prices/CusPriceService.js";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
|
||||
export const cusRouter = Router();
|
||||
|
||||
@@ -305,6 +310,7 @@ cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Update customer entitlement directly
|
||||
cusRouter.post(
|
||||
"/customer_entitlements/:customer_entitlement_id",
|
||||
async (req: any, res: any) => {
|
||||
@@ -332,19 +338,52 @@ cusRouter.post(
|
||||
}
|
||||
|
||||
// Check if org owns the entitlement
|
||||
await CustomerEntitlementService.getByIdStrict({
|
||||
const cusEnt = await CustomerEntitlementService.getByIdStrict({
|
||||
sb: req.sb,
|
||||
id: customer_entitlement_id,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
});
|
||||
|
||||
const amountUsed = cusEnt.balance! - balance;
|
||||
|
||||
await CustomerEntitlementService.update({
|
||||
sb: req.sb,
|
||||
id: customer_entitlement_id,
|
||||
updates: { balance, next_reset_at },
|
||||
});
|
||||
|
||||
if (cusEnt.usage_allowed) {
|
||||
// Get related usage price
|
||||
const cusPrices: FullCustomerPrice[] =
|
||||
await CusPriceService.getByCusProductId({
|
||||
sb: req.sb,
|
||||
customerProductId: cusEnt.customer_product_id,
|
||||
});
|
||||
|
||||
const relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
|
||||
|
||||
if (!relatedCusPrice) {
|
||||
res.status(200).json({ success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const fullOrg = await OrgService.getFullOrg({
|
||||
sb: req.sb,
|
||||
orgId: req.orgId,
|
||||
});
|
||||
|
||||
await updateCusEntInStripe({
|
||||
cusEnt,
|
||||
cusPrices,
|
||||
org: fullOrg,
|
||||
env: req.env,
|
||||
customer: cusEnt.customer,
|
||||
amountUsed,
|
||||
eventId: generateId("manual"),
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
handleRequestError({
|
||||
@@ -369,6 +408,11 @@ cusRouter.post("/:customer_id/balances", async (req: any, res: any) => {
|
||||
env: req.env,
|
||||
});
|
||||
|
||||
const fullOrg = await OrgService.getFullOrg({
|
||||
sb: req.sb,
|
||||
orgId: req.orgId,
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new RecaseError({
|
||||
message: `Customer ${cusId} not found`,
|
||||
@@ -382,14 +426,12 @@ cusRouter.post("/:customer_id/balances", async (req: any, res: any) => {
|
||||
balances.map((b: any) => b.feature_id).includes(f.id)
|
||||
);
|
||||
|
||||
const cusEnts = await CustomerEntitlementService.getActiveInFeatureIds({
|
||||
const { cusEnts, cusPrices } = await getCusEntsAndPrices({
|
||||
sb: req.sb,
|
||||
internalCustomerId: customer.internal_id,
|
||||
internalFeatureIds: featuresToUpdate.map((f) => f.internal_id),
|
||||
});
|
||||
|
||||
sortCusEntsForDeduction(cusEnts);
|
||||
|
||||
// console.log("cusEnts", cusEnts);
|
||||
for (const balance of balances) {
|
||||
if (!balance.feature_id) {
|
||||
@@ -416,21 +458,24 @@ cusRouter.post("/:customer_id/balances", async (req: any, res: any) => {
|
||||
let newBalance = balance.balance;
|
||||
for (const cusEnt of cusEnts) {
|
||||
if (cusEnt.internal_feature_id === feature.internal_id) {
|
||||
curBalance += cusEnt.balance;
|
||||
curBalance += cusEnt.balance!;
|
||||
}
|
||||
}
|
||||
|
||||
let updateAmount = newBalance - curBalance;
|
||||
let toDeduct = curBalance - newBalance;
|
||||
|
||||
for (const cusEnt of cusEnts) {
|
||||
if (updateAmount == 0) break;
|
||||
if (toDeduct == 0) break;
|
||||
if (cusEnt.internal_feature_id === feature.internal_id) {
|
||||
if (cusEnt.balance + updateAmount < 0) {
|
||||
updateAmount += cusEnt.balance;
|
||||
let amountUsed;
|
||||
if (cusEnt.balance! - toDeduct < 0) {
|
||||
toDeduct -= cusEnt.balance!;
|
||||
amountUsed = cusEnt.balance!;
|
||||
newBalance = 0;
|
||||
} else {
|
||||
newBalance = cusEnt.balance + updateAmount;
|
||||
updateAmount = 0;
|
||||
newBalance = cusEnt.balance! - toDeduct;
|
||||
amountUsed = toDeduct;
|
||||
toDeduct = 0;
|
||||
}
|
||||
|
||||
await CustomerEntitlementService.update({
|
||||
@@ -440,6 +485,21 @@ cusRouter.post("/:customer_id/balances", async (req: any, res: any) => {
|
||||
balance: newBalance,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Amount used", amountUsed);
|
||||
console.log("Feature", feature.name);
|
||||
|
||||
if (cusEnt.usage_allowed) {
|
||||
await updateCusEntInStripe({
|
||||
cusEnt,
|
||||
cusPrices,
|
||||
org: fullOrg,
|
||||
env: req.env,
|
||||
customer,
|
||||
amountUsed,
|
||||
eventId: generateId("manual"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ import {
|
||||
CusProductSchema,
|
||||
Customer,
|
||||
CustomerSchema,
|
||||
FullCustomerEntitlement,
|
||||
FullCustomerPrice,
|
||||
Organization,
|
||||
ProductSchema,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { CreateCustomer } from "@autumn/shared";
|
||||
@@ -154,3 +157,44 @@ export const flipProductResults = (
|
||||
}
|
||||
return customers;
|
||||
};
|
||||
|
||||
export const getCusEntsAndPrices = async ({
|
||||
sb,
|
||||
internalCustomerId,
|
||||
internalFeatureIds,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
internalCustomerId: string;
|
||||
internalFeatureIds: string[];
|
||||
}) => {
|
||||
const cusWithProducts = await CusService.getActiveProductsByInternalId({
|
||||
sb,
|
||||
internalCustomerId,
|
||||
});
|
||||
|
||||
if (!cusWithProducts) {
|
||||
return { cusEnts: [], cusPrices: [] };
|
||||
}
|
||||
|
||||
const cusProducts = cusWithProducts?.customer_products;
|
||||
|
||||
const cusEnts: FullCustomerEntitlement[] = [];
|
||||
const cusPrices: FullCustomerPrice[] = [];
|
||||
for (const cusProduct of cusProducts) {
|
||||
cusEnts.push(
|
||||
...cusProduct.customer_entitlements.filter(
|
||||
(cusEnt: FullCustomerEntitlement) =>
|
||||
internalFeatureIds.includes(cusEnt.entitlement.internal_feature_id!)
|
||||
)
|
||||
);
|
||||
cusPrices.push(
|
||||
...cusProduct.customer_prices.filter((cusPrice: FullCustomerPrice) => {
|
||||
const priceConfig = cusPrice.price.config as UsagePriceConfig;
|
||||
|
||||
return internalFeatureIds.includes(priceConfig.internal_feature_id);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { cusEnts, cusPrices };
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Client } from "pg";
|
||||
import { Queue } from "bullmq";
|
||||
import { createNewCustomer } from "../customers/cusUtils.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
|
||||
export const eventsRouter = Router();
|
||||
|
||||
@@ -121,6 +122,11 @@ export const handleEventSent = async ({
|
||||
}) => {
|
||||
const { sb, pg, orgId, env } = req;
|
||||
|
||||
const org = await OrgService.getFullOrg({
|
||||
sb,
|
||||
orgId,
|
||||
});
|
||||
|
||||
const { customer, event } = await getEventAndCustomer({
|
||||
sb,
|
||||
orgId,
|
||||
@@ -144,6 +150,8 @@ export const handleEventSent = async ({
|
||||
customer,
|
||||
features: affectedFeatures,
|
||||
event,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -532,4 +532,43 @@ export class CusService {
|
||||
}
|
||||
|
||||
// ENTITLEMENTS
|
||||
|
||||
// Get active products
|
||||
static async getActiveProductsByInternalId({
|
||||
sb,
|
||||
internalCustomerId,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
internalCustomerId: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("customers")
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
customer_products:customer_products!inner(*,
|
||||
customer_prices:customer_prices!inner(*,
|
||||
price:prices!inner(*)
|
||||
),
|
||||
customer_entitlements:customer_entitlements!inner(*,
|
||||
entitlement:entitlements!inner(*,
|
||||
feature:features!inner(*)
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq("internal_id", internalCustomerId)
|
||||
.eq("customer_products.status", CusProductStatus.Active)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code === "PGRST116") {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CusProduct,
|
||||
FeatureOptions,
|
||||
FreeTrial,
|
||||
BillingType,
|
||||
} from "@autumn/shared";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
import { getNextEntitlementReset } from "@/utils/timeUtils.js";
|
||||
@@ -17,12 +18,15 @@ import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { getEntOptions } from "@/internal/prices/priceUtils.js";
|
||||
import { getBillingType, getEntOptions } from "@/internal/prices/priceUtils.js";
|
||||
import { CustomerPrice } from "@autumn/shared";
|
||||
import { CusProductService } from "../products/CusProductService.js";
|
||||
import { AttachParams } from "../products/AttachParams.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { applyTrialToEntitlement } from "@/internal/products/entitlements/entitlementUtils.js";
|
||||
import {
|
||||
applyTrialToEntitlement,
|
||||
getEntRelatedPrice,
|
||||
} from "@/internal/products/entitlements/entitlementUtils.js";
|
||||
|
||||
export const initCusEntitlement = ({
|
||||
entitlement,
|
||||
@@ -32,6 +36,7 @@ export const initCusEntitlement = ({
|
||||
options,
|
||||
nextResetAt,
|
||||
billLaterOnly = false,
|
||||
relatedPrice,
|
||||
}: {
|
||||
entitlement: EntitlementWithFeature;
|
||||
customer: Customer;
|
||||
@@ -40,6 +45,7 @@ export const initCusEntitlement = ({
|
||||
options?: FeatureOptions;
|
||||
nextResetAt?: number;
|
||||
billLaterOnly?: boolean;
|
||||
relatedPrice?: Price;
|
||||
}) => {
|
||||
const feature: Feature = entitlement.feature;
|
||||
|
||||
@@ -77,6 +83,14 @@ export const initCusEntitlement = ({
|
||||
entitlement.allowance_type === AllowanceType.Unlimited ||
|
||||
entitlement.interval == EntInterval.Lifetime;
|
||||
|
||||
let usageAllowed = false;
|
||||
if (
|
||||
relatedPrice &&
|
||||
getBillingType(relatedPrice.config!) === BillingType.UsageInArrear
|
||||
) {
|
||||
usageAllowed = true;
|
||||
}
|
||||
|
||||
return {
|
||||
id: generateId("cus_ent"),
|
||||
internal_customer_id: customer.internal_id,
|
||||
@@ -94,7 +108,7 @@ export const initCusEntitlement = ({
|
||||
? null
|
||||
: entitlement.allowance_type === AllowanceType.Unlimited,
|
||||
balance: isBooleanFeature ? null : balance,
|
||||
usage_allowed: isBooleanFeature ? null : false,
|
||||
usage_allowed: usageAllowed,
|
||||
next_reset_at: nextResetNull ? null : nextResetAt || nextResetAtCalculated,
|
||||
};
|
||||
};
|
||||
@@ -309,6 +323,7 @@ export const createFullCusProduct = async ({
|
||||
|
||||
for (const entitlement of entitlements) {
|
||||
const options = getEntOptions(optionsList, entitlement);
|
||||
const relatedPrice = getEntRelatedPrice(entitlement, prices);
|
||||
|
||||
const cusEnt: any = initCusEntitlement({
|
||||
entitlement,
|
||||
@@ -318,6 +333,7 @@ export const createFullCusProduct = async ({
|
||||
nextResetAt,
|
||||
billLaterOnly,
|
||||
freeTrial: disableFreeTrial ? null : freeTrial,
|
||||
relatedPrice,
|
||||
});
|
||||
|
||||
cusEnts.push(cusEnt);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { CustomerEntitlement, ErrCode } from "@autumn/shared";
|
||||
import { CusProductStatus, CustomerEntitlement, ErrCode } from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
@@ -178,7 +178,6 @@ export class CustomerEntitlementService {
|
||||
orgId: string;
|
||||
env: string;
|
||||
}) {
|
||||
console.log("Getting entitlements for customer: ", customerId);
|
||||
const { data, error } = await sb
|
||||
.from("customer_entitlements")
|
||||
.select(
|
||||
@@ -336,7 +335,9 @@ export class CustomerEntitlementService {
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("customer_entitlements")
|
||||
.select("*, customer:customers!inner(*)")
|
||||
.select(
|
||||
"*, customer:customers!inner(*), entitlement:entitlements!inner(*)"
|
||||
)
|
||||
.eq("id", id)
|
||||
.eq("customer.org_id", orgId)
|
||||
.eq("customer.env", env)
|
||||
@@ -355,4 +356,23 @@ export class CustomerEntitlementService {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getByCusProductId({
|
||||
sb,
|
||||
cusProductId,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
cusProductId: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("customer_entitlements")
|
||||
.select("*, entitlement:entitlements!inner(*)")
|
||||
.eq("customer_product_id", cusProductId);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ import { CustomerEntitlementService } from "./CusEntitlementService.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import {
|
||||
AllowanceType,
|
||||
BillingInterval,
|
||||
CustomerEntitlement,
|
||||
AppEnv,
|
||||
Customer,
|
||||
EntInterval,
|
||||
EntitlementWithFeature,
|
||||
FeatureType,
|
||||
FullCustomerEntitlement,
|
||||
FullCustomerPrice,
|
||||
Organization,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { getEntOptions } from "@/internal/prices/priceUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
|
||||
export const getFeatureBalance = async ({
|
||||
pg,
|
||||
@@ -373,3 +377,61 @@ export const sortCusEntsForDeduction = (cusEnts: FullCustomerEntitlement[]) => {
|
||||
return a.created_at - b.created_at;
|
||||
});
|
||||
};
|
||||
|
||||
// Get related cusPrice
|
||||
export const getRelatedCusPrice = (
|
||||
cusEnt: FullCustomerEntitlement,
|
||||
cusPrices: FullCustomerPrice[]
|
||||
) => {
|
||||
return cusPrices.find((cusPrice) => {
|
||||
if (cusPrice.customer_product_id == cusEnt.customer_product_id) {
|
||||
let config = cusPrice.price.config as UsagePriceConfig;
|
||||
return (
|
||||
config.internal_feature_id == cusEnt.entitlement.internal_feature_id
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// 3. Perform deductions and update customer balance
|
||||
export const updateCusEntInStripe = async ({
|
||||
cusEnt,
|
||||
cusPrices,
|
||||
org,
|
||||
env,
|
||||
customer,
|
||||
amountUsed,
|
||||
eventId,
|
||||
}: {
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
cusPrices: FullCustomerPrice[];
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
customer: Customer;
|
||||
amountUsed: number;
|
||||
eventId: string;
|
||||
}) => {
|
||||
const relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
|
||||
|
||||
if (!relatedCusPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send event to Stripe
|
||||
const stripeCli = createStripeCli({
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
await stripeCli.billing.meterEvents.create({
|
||||
event_name: relatedCusPrice.price.id!,
|
||||
payload: {
|
||||
stripe_customer_id: customer.processor.id,
|
||||
value: amountUsed.toString(),
|
||||
},
|
||||
identifier: eventId,
|
||||
});
|
||||
console.log(` ✅ Stripe event sent, amount: (${amountUsed})`);
|
||||
};
|
||||
|
||||
30
server/src/internal/customers/prices/CusPriceService.tsx
Normal file
30
server/src/internal/customers/prices/CusPriceService.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
export class CusPriceService {
|
||||
static async getByCusProductId({
|
||||
sb,
|
||||
customerProductId,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
customerProductId: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("customer_prices")
|
||||
.select("*, price:prices(*)")
|
||||
.eq("customer_product_id", customerProductId);
|
||||
|
||||
if (error) {
|
||||
throw new RecaseError({
|
||||
message: "Error getting customer prices",
|
||||
code: ErrCode.GetCusPriceFailed,
|
||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -236,6 +236,32 @@ export class CusProductService {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getEntsAndPrices({
|
||||
sb,
|
||||
cusProductId,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
cusProductId: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("customer_products")
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
customer_entitlements:customer_entitlements!inner(*, entitlement:entitlements!inner(*)),
|
||||
customer_prices:customer_prices!inner(*, price:prices!inner(*))
|
||||
`
|
||||
)
|
||||
.eq("id", cusProductId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getPastDueByInvoiceId({
|
||||
sb,
|
||||
invoiceId,
|
||||
|
||||
@@ -17,12 +17,17 @@ import {
|
||||
UsagePriceConfigSchema,
|
||||
} from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { getBillingType, priceToStripeTiers } from "./priceUtils.js";
|
||||
import {
|
||||
getBillingType,
|
||||
priceToStripeTiers,
|
||||
roundPriceAmounts,
|
||||
} from "./priceUtils.js";
|
||||
import { PriceService } from "./PriceService.js";
|
||||
import {
|
||||
billingIntervalToStripe,
|
||||
createStripeCli,
|
||||
} from "@/external/stripe/utils.js";
|
||||
import { createStripeMeteredPrice } from "@/external/stripe/stripePriceUtils.js";
|
||||
|
||||
// GET PRICES
|
||||
const validatePrice = (price: Price) => {
|
||||
@@ -155,48 +160,61 @@ const handleStripePrices = async ({
|
||||
for (const price of prices) {
|
||||
const config = price.config! as UsagePriceConfig;
|
||||
const billingType = getBillingType(config);
|
||||
|
||||
// If price.config.meter_id and stripe_price_id, delete
|
||||
|
||||
if (billingType == BillingType.UsageInArrear) {
|
||||
const feature = features.find(
|
||||
(f) => f.internal_id === config.internal_feature_id
|
||||
);
|
||||
if (!config.stripe_price_id) {
|
||||
const feature = features.find(
|
||||
(f) => f.internal_id === config.internal_feature_id
|
||||
);
|
||||
|
||||
const meter = await stripeCli.billing.meters.create({
|
||||
display_name: `${product.name} - ${feature!.name}`,
|
||||
event_name: price.id!,
|
||||
default_aggregation: {
|
||||
formula: "sum",
|
||||
},
|
||||
});
|
||||
const meter = await stripeCli.billing.meters.create({
|
||||
display_name: `${product.name} - ${feature!.name}`,
|
||||
event_name: price.id!,
|
||||
default_aggregation: {
|
||||
formula: "sum",
|
||||
},
|
||||
});
|
||||
|
||||
const stripePrice = await stripeCli.prices.create({
|
||||
// product: product.processor!.id,
|
||||
product_data: {
|
||||
name: `${product.name} - ${feature!.name}`,
|
||||
},
|
||||
// unit_amount: ,
|
||||
billing_scheme: "tiered",
|
||||
tiers_mode: "volume",
|
||||
tiers: priceToStripeTiers(
|
||||
const stripePrice = await createStripeMeteredPrice({
|
||||
stripeCli,
|
||||
product,
|
||||
price,
|
||||
entitlements.find(
|
||||
(e) => e.internal_feature_id === feature!.internal_id
|
||||
)!
|
||||
),
|
||||
currency: "usd",
|
||||
recurring: {
|
||||
...(billingIntervalToStripe(config.interval!) as any),
|
||||
meter: meter.id,
|
||||
usage_type: "metered",
|
||||
},
|
||||
});
|
||||
entitlements,
|
||||
feature: feature!,
|
||||
meterId: meter.id,
|
||||
});
|
||||
|
||||
let newUsageConfig = {
|
||||
...config,
|
||||
stripe_meter_id: meter.id,
|
||||
stripe_price_id: stripePrice.id,
|
||||
};
|
||||
let newUsageConfig = {
|
||||
...config,
|
||||
stripe_meter_id: meter.id,
|
||||
stripe_price_id: stripePrice.id,
|
||||
};
|
||||
|
||||
price.config = newUsageConfig;
|
||||
price.config = newUsageConfig;
|
||||
} else {
|
||||
// Update price
|
||||
// Set old price to inactive
|
||||
await stripeCli.prices.update(config.stripe_price_id, {
|
||||
active: false,
|
||||
});
|
||||
|
||||
const feature = features.find(
|
||||
(f) => f.internal_id === config.internal_feature_id
|
||||
);
|
||||
|
||||
const stripePrice = await createStripeMeteredPrice({
|
||||
stripeCli,
|
||||
product,
|
||||
price,
|
||||
entitlements,
|
||||
feature: feature!,
|
||||
meterId: config.stripe_meter_id!,
|
||||
});
|
||||
|
||||
config.stripe_price_id = stripePrice.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -243,6 +261,7 @@ export const handleNewPrices = async ({
|
||||
for (let newPrice of newPrices) {
|
||||
// Validate price
|
||||
validatePrice(newPrice);
|
||||
roundPriceAmounts(newPrice);
|
||||
|
||||
// 1. Handle new price
|
||||
if (!("id" in newPrice)) {
|
||||
|
||||
@@ -271,7 +271,7 @@ export const getPriceAmount = (price: Price, options: FeatureOptions) => {
|
||||
if (price.billing_type == BillingType.OneOff) {
|
||||
let config = price.config as FixedPriceConfig;
|
||||
return {
|
||||
amountPerUnit: config.amount,
|
||||
amountPerUnit: Number(config.amount.toFixed(2)),
|
||||
quantity: 1,
|
||||
};
|
||||
} else if (price.billing_type == BillingType.UsageInAdvance) {
|
||||
@@ -279,7 +279,7 @@ export const getPriceAmount = (price: Price, options: FeatureOptions) => {
|
||||
let usageTier = getUsageTier(price, quantity);
|
||||
|
||||
return {
|
||||
amountPerUnit: usageTier.amount,
|
||||
amountPerUnit: Number(usageTier.amount.toFixed(2)),
|
||||
quantity: quantity,
|
||||
};
|
||||
}
|
||||
@@ -314,9 +314,28 @@ export const priceToStripeTiers = (price: Price, entitlement: Entitlement) => {
|
||||
up_to: tier.to == -1 ? "inf" : tier.to,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Tiers: ", tiers);
|
||||
return tiers;
|
||||
};
|
||||
|
||||
export const priceToEventName = (productName: string, featureName: string) => {
|
||||
return `${productName} - ${featureName}`;
|
||||
};
|
||||
|
||||
export const roundPriceAmounts = (price: Price) => {
|
||||
if (price.config!.type == PriceType.Fixed) {
|
||||
const config = price.config as FixedPriceConfig;
|
||||
config.amount = Number(config.amount.toFixed(2));
|
||||
price.config = config;
|
||||
} else if (price.config!.type == PriceType.Usage) {
|
||||
const config = price.config as UsagePriceConfig;
|
||||
for (let i = 0; i < config.usage_tiers.length; i++) {
|
||||
config.usage_tiers[i].amount = Number(
|
||||
config.usage_tiers[i].amount.toFixed(2)
|
||||
);
|
||||
}
|
||||
|
||||
price.config = config;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
Feature,
|
||||
ErrCode,
|
||||
EntitlementSchema,
|
||||
UsagePriceConfig,
|
||||
PriceType,
|
||||
Price,
|
||||
} from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { addDays } from "date-fns";
|
||||
@@ -258,3 +261,18 @@ export const handleNewEntitlements = async ({
|
||||
`Successfully handled new entitlements. Created ${createdEnts.length}, updated ${updatedEnts.length}, removed ${removedEnts.length}`
|
||||
);
|
||||
};
|
||||
|
||||
// OTHERS
|
||||
export const getEntRelatedPrice = (
|
||||
entitlement: Entitlement,
|
||||
prices: Price[]
|
||||
) => {
|
||||
return prices.find((price) => {
|
||||
if (price.config?.type === PriceType.Fixed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const config = price.config as UsagePriceConfig;
|
||||
return config.internal_feature_id === entitlement.internal_feature_id;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -70,6 +70,8 @@ const initWorker = (id: number, queue: Queue) => {
|
||||
|
||||
try {
|
||||
await runUpdateBalanceTask(job.data);
|
||||
} catch (error) {
|
||||
console.error("Error updating balance:", error);
|
||||
} finally {
|
||||
await releaseLock(customerId);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,31 @@ import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
||||
import { handleBelowThresholdInvoicing } from "./invoiceThresholdUtils.js";
|
||||
import { getBelowThresholdPrice } from "./invoiceThresholdUtils.js";
|
||||
|
||||
import { AggregateType, AllowanceType, Event, Feature } from "@autumn/shared";
|
||||
import {
|
||||
AggregateType,
|
||||
AllowanceType,
|
||||
AppEnv,
|
||||
CusEntWithEntitlement,
|
||||
Event,
|
||||
Feature,
|
||||
FullCustomerEntitlement,
|
||||
FullCustomerPrice,
|
||||
Organization,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { CustomerEntitlementService } from "@/internal/customers/entitlements/CusEntitlementService.js";
|
||||
import { Customer, FeatureType } from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { SbChannelEvent } from "@/websockets/initWs.js";
|
||||
import chalk from "chalk";
|
||||
import { sortCusEntsForDeduction } from "@/internal/customers/entitlements/cusEntUtils.js";
|
||||
import {
|
||||
getRelatedCusPrice,
|
||||
sortCusEntsForDeduction,
|
||||
updateCusEntInStripe,
|
||||
} from "@/internal/customers/entitlements/cusEntUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { getCusEntsAndPrices } from "@/internal/api/customers/cusUtils.js";
|
||||
|
||||
// 3. Get customer entitlements and sort
|
||||
const getCustomerEntitlements = async ({
|
||||
@@ -88,135 +106,216 @@ const getCreditSystemDeduction = ({
|
||||
return creditsUpdate;
|
||||
};
|
||||
|
||||
// 1. Main function to update customer balance
|
||||
// // 1. Get customer entitlements and prices
|
||||
// const getCustomerEntitlementsAndPrices = async ({
|
||||
// sb,
|
||||
// internalCustomerId,
|
||||
// features,
|
||||
// }: {
|
||||
// sb: SupabaseClient;
|
||||
// internalCustomerId: string;
|
||||
// features: Feature[];
|
||||
// }) => {
|
||||
// const internalFeatureIds = features.map((feature) => feature.internal_id);
|
||||
// const cusWithProducts = await CusService.getActiveProductsByInternalId({
|
||||
// sb,
|
||||
// internalCustomerId,
|
||||
// });
|
||||
|
||||
// if (!cusWithProducts) {
|
||||
// return { cusEnts: [], cusPrices: [] };
|
||||
// }
|
||||
|
||||
// const cusProducts = cusWithProducts?.customer_products;
|
||||
|
||||
// const cusEnts: FullCustomerEntitlement[] = [];
|
||||
// const cusPrices: FullCustomerPrice[] = [];
|
||||
// for (const cusProduct of cusProducts) {
|
||||
// cusEnts.push(
|
||||
// ...cusProduct.customer_entitlements.filter(
|
||||
// (cusEnt: FullCustomerEntitlement) =>
|
||||
// internalFeatureIds.includes(cusEnt.entitlement.internal_feature_id)
|
||||
// )
|
||||
// );
|
||||
// cusPrices.push(
|
||||
// ...cusProduct.customer_prices.filter((cusPrice: FullCustomerPrice) => {
|
||||
// const priceConfig = cusPrice.price.config as UsagePriceConfig;
|
||||
|
||||
// return internalFeatureIds.includes(priceConfig.internal_feature_id);
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
|
||||
// sortCusEntsForDeduction(cusEnts);
|
||||
|
||||
// return { cusEnts, cusPrices };
|
||||
// };
|
||||
|
||||
// 2. Get deductions for each feature
|
||||
const getFeatureDeductions = ({
|
||||
cusEnts,
|
||||
event,
|
||||
features,
|
||||
}: {
|
||||
cusEnts: FullCustomerEntitlement[];
|
||||
event: Event;
|
||||
features: Feature[];
|
||||
}) => {
|
||||
const meteredFeatures = features.filter(
|
||||
(feature) => feature.type === FeatureType.Metered
|
||||
);
|
||||
const featureDeductions = [];
|
||||
for (const feature of features) {
|
||||
let deduction;
|
||||
if (feature.type === FeatureType.Metered) {
|
||||
deduction = getMeteredDeduction(feature, event);
|
||||
} else if (feature.type === FeatureType.CreditSystem) {
|
||||
deduction = getCreditSystemDeduction({
|
||||
meteredFeatures: meteredFeatures,
|
||||
creditSystem: feature,
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if unlimited exists
|
||||
let unlimitedExists = cusEnts.some(
|
||||
(cusEnt) => cusEnt.entitlement.allowance_type === AllowanceType.Unlimited
|
||||
);
|
||||
|
||||
if (unlimitedExists || !deduction) {
|
||||
continue;
|
||||
}
|
||||
|
||||
featureDeductions.push({
|
||||
feature,
|
||||
deduction,
|
||||
});
|
||||
}
|
||||
|
||||
return featureDeductions;
|
||||
};
|
||||
|
||||
// // 3. Perform deductions and update customer balance
|
||||
// const handleUsageAllowedCusEnt = async ({
|
||||
// cusEnt,
|
||||
// cusPrices,
|
||||
// org,
|
||||
// env,
|
||||
// customer,
|
||||
// amountUsed,
|
||||
// }: {
|
||||
// cusEnt: FullCustomerEntitlement;
|
||||
// cusPrices: FullCustomerPrice[];
|
||||
// org: Organization;
|
||||
// env: AppEnv;
|
||||
// customer: Customer;
|
||||
// amountUsed: number;
|
||||
// }) => {
|
||||
// const relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
|
||||
|
||||
// if (!relatedCusPrice) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Send event to Stripe
|
||||
// const stripeCli = createStripeCli({
|
||||
// org,
|
||||
// env,
|
||||
// });
|
||||
|
||||
// await stripeCli.billing.meterEvents.create({
|
||||
// event_name: relatedCusPrice.price.id!,
|
||||
// payload: {
|
||||
// stripe_customer_id: customer.processor.id,
|
||||
// value: amountUsed.toString(),
|
||||
// },
|
||||
// });
|
||||
// console.log(" ✅ Stripe event sent");
|
||||
// };
|
||||
|
||||
// Main function to update customer balance
|
||||
export const updateCustomerBalance = async ({
|
||||
sb,
|
||||
customer,
|
||||
event,
|
||||
features,
|
||||
org,
|
||||
env,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
customer: Customer;
|
||||
event: Event;
|
||||
features: Feature[];
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const cusEnts = await getCustomerEntitlements({
|
||||
const startTime = performance.now();
|
||||
const { cusEnts, cusPrices } = await getCusEntsAndPrices({
|
||||
sb,
|
||||
internalCustomerId: customer.internal_id,
|
||||
features,
|
||||
internalFeatureIds: features.map((f) => f.internal_id!),
|
||||
});
|
||||
|
||||
const endTime = performance.now();
|
||||
console.log(` - Cus ents func ${(endTime - startTime).toFixed(2)}ms`);
|
||||
|
||||
if (cusEnts.length === 0 || features.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = sb.channel(
|
||||
`${customer.org_id}_${customer.env}_${customer.id}`
|
||||
);
|
||||
|
||||
// Update customer balance
|
||||
const featureIdToDeduction: any = {};
|
||||
const meteredFeatures = features.filter(
|
||||
(feature) => feature.type === FeatureType.Metered
|
||||
);
|
||||
|
||||
console.log(` - Customer: ${customer.name} (${customer.internal_id})`);
|
||||
console.log(` - Features: ${features.map((f) => f.id).join(", ")}`);
|
||||
|
||||
for (const cusEnt of cusEnts) {
|
||||
const internalFeatureId = cusEnt.internal_feature_id;
|
||||
if (featureIdToDeduction[internalFeatureId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const feature = cusEnt.entitlement.feature;
|
||||
|
||||
// 1. Skip if customer has unlimited entitlement
|
||||
let unlimitedExists = false;
|
||||
for (const cusEnt of cusEnts) {
|
||||
if (cusEnt.entitlement.allowance_type == AllowanceType.Unlimited) {
|
||||
unlimitedExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (unlimitedExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Get metered feature deduction
|
||||
if (feature.type === FeatureType.Metered) {
|
||||
let deduction = getMeteredDeduction(feature, event);
|
||||
|
||||
featureIdToDeduction[internalFeatureId] = {
|
||||
cusEntId: cusEnt.id,
|
||||
deduction,
|
||||
feature: feature,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Get credit system deduction
|
||||
if (feature.type === FeatureType.CreditSystem) {
|
||||
const deduction = getCreditSystemDeduction({
|
||||
meteredFeatures,
|
||||
creditSystem: feature,
|
||||
event,
|
||||
});
|
||||
|
||||
featureIdToDeduction[internalFeatureId] = {
|
||||
cusEntId: cusEnt.id,
|
||||
deduction: deduction,
|
||||
feature: feature,
|
||||
};
|
||||
}
|
||||
|
||||
let deduction = featureIdToDeduction[internalFeatureId]?.deduction;
|
||||
let curBalance = cusEnt.balance!;
|
||||
|
||||
if (curBalance === undefined || curBalance === null || !deduction) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Feature ID to Deduction
|
||||
const deductions: {
|
||||
deduction: number;
|
||||
feature: Feature;
|
||||
}[] = Object.values(featureIdToDeduction);
|
||||
const featureDeductions = getFeatureDeductions({
|
||||
cusEnts,
|
||||
event,
|
||||
features,
|
||||
});
|
||||
|
||||
for (const obj of deductions) {
|
||||
console.log(
|
||||
" - Deductions:",
|
||||
featureDeductions.map((f) => `${f.feature.id}: ${f.deduction}`)
|
||||
);
|
||||
|
||||
// 3. Perform deductions and update customer balance
|
||||
for (const obj of featureDeductions) {
|
||||
if (!obj.deduction) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let toDeduct = obj.deduction;
|
||||
|
||||
// 1. Deduct from entitlement (till 0)
|
||||
for (const cusEnt of cusEnts) {
|
||||
if (cusEnt.internal_feature_id === obj.feature.internal_id) {
|
||||
// If deduction finished or cusent has no more balance, break
|
||||
|
||||
if (toDeduct == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (cusEnt.balance == 0) {
|
||||
if (cusEnt.balance! <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let newBalance;
|
||||
let newBalance, deducted;
|
||||
|
||||
// If cusEnt has less balance to deduct than 0, deduct the balance and set balance to 0
|
||||
if (cusEnt.balance - toDeduct < 0) {
|
||||
toDeduct -= cusEnt.balance;
|
||||
if (cusEnt.balance! - toDeduct < 0) {
|
||||
toDeduct -= cusEnt.balance!;
|
||||
deducted = cusEnt.balance!;
|
||||
newBalance = 0;
|
||||
}
|
||||
|
||||
// Else, deduct the balance and set toDeduct to 0
|
||||
else {
|
||||
newBalance = cusEnt.balance - toDeduct;
|
||||
newBalance = cusEnt.balance! - toDeduct;
|
||||
deducted = toDeduct;
|
||||
toDeduct = 0;
|
||||
}
|
||||
|
||||
cusEnt.balance = newBalance;
|
||||
|
||||
await CustomerEntitlementService.update({
|
||||
sb,
|
||||
id: cusEnt.id,
|
||||
@@ -224,6 +323,19 @@ export const updateCustomerBalance = async ({
|
||||
balance: newBalance,
|
||||
},
|
||||
});
|
||||
|
||||
// // If cus ent has usage_allowed -> update balance
|
||||
// if (cusEnt.usage_allowed) {
|
||||
// await updateCusEntInStripe({
|
||||
// cusEnt,
|
||||
// cusPrices,
|
||||
// org,
|
||||
// env,
|
||||
// customer,
|
||||
// amountUsed: deducted,
|
||||
// eventId: event.id + "_1",
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,12 +346,15 @@ export const updateCustomerBalance = async ({
|
||||
|
||||
// Deduct from usage-based price
|
||||
const usageBasedEnt = cusEnts.find(
|
||||
(cusEnt) => cusEnt.entitlement.usage_allowed
|
||||
(cusEnt: CusEntWithEntitlement) => cusEnt.usage_allowed
|
||||
);
|
||||
|
||||
if (usageBasedEnt) {
|
||||
console.log("Deducting from usage-based price (entitlement)");
|
||||
let newBalance = usageBasedEnt.balance - toDeduct;
|
||||
let newBalance = usageBasedEnt.balance! - toDeduct;
|
||||
// console.log("Cur balance", usageBasedEnt.balance);
|
||||
// console.log("To deduct", toDeduct);
|
||||
// console.log("New balance", newBalance);
|
||||
|
||||
await CustomerEntitlementService.update({
|
||||
sb,
|
||||
id: usageBasedEnt.id,
|
||||
@@ -247,25 +362,31 @@ export const updateCustomerBalance = async ({
|
||||
balance: newBalance,
|
||||
},
|
||||
});
|
||||
|
||||
// await updateCusEntInStripe({
|
||||
// cusEnt: usageBasedEnt,
|
||||
// cusPrices,
|
||||
// org,
|
||||
// env,
|
||||
// customer,
|
||||
// amountUsed: toDeduct,
|
||||
// eventId: event.id + "_2",
|
||||
// });
|
||||
} else {
|
||||
console.log("No usage-based entitlement found");
|
||||
}
|
||||
}
|
||||
|
||||
let featuresUpdated = Object.values(featureIdToDeduction).map(
|
||||
(obj: any) => `(${obj.feature.id}: ${obj.deduction})`
|
||||
);
|
||||
|
||||
console.log(` - Deducted ${featuresUpdated}`);
|
||||
return cusEnts;
|
||||
};
|
||||
|
||||
// MAIN FUNCTION
|
||||
export const runUpdateBalanceTask = async (payload: any) => {
|
||||
try {
|
||||
const sb = createSupabaseClient();
|
||||
|
||||
// 1. Update customer balance
|
||||
const { customer, features, event } = payload;
|
||||
const { customer, features, event, org, env } = payload;
|
||||
|
||||
console.log("--------------------------------");
|
||||
console.log("Inside updateBalanceTask...");
|
||||
@@ -276,6 +397,8 @@ export const runUpdateBalanceTask = async (payload: any) => {
|
||||
customer,
|
||||
features,
|
||||
event,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
if (!cusEnts || cusEnts.length === 0) {
|
||||
@@ -305,7 +428,7 @@ export const runUpdateBalanceTask = async (payload: any) => {
|
||||
console.log(" ✅ No below threshold price found");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error updating customer balance: ${error}`);
|
||||
console.log(`Error updating customer balance`);
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,4 +69,7 @@ export const ErrCode = {
|
||||
|
||||
// Cus Product
|
||||
NoActiveCusProducts: "no_active_cus_products",
|
||||
|
||||
// Cus Price
|
||||
GetCusPriceFailed: "get_cus_price_failed",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user