Merge branch 'staging' into feat/plan-editor
This commit is contained in:
27
server/src/external/autumn/autumnCli.ts
vendored
27
server/src/external/autumn/autumnCli.ts
vendored
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/suspicious/noExplicitAny: AutumnInt is used for internal testing & scripts */
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
@@ -50,12 +51,14 @@ export class AutumnInt {
|
||||
baseUrl,
|
||||
version,
|
||||
orgConfig,
|
||||
liveUrl = false,
|
||||
}: {
|
||||
apiKey?: string;
|
||||
secretKey?: string;
|
||||
baseUrl?: string;
|
||||
version?: string | APIVersion;
|
||||
orgConfig?: Partial<OrgConfig>;
|
||||
liveUrl?: boolean;
|
||||
} = {}) {
|
||||
// this.apiKey = apiKey || process.env.AUTUMN_API_KEY || "";
|
||||
this.apiKey =
|
||||
@@ -74,7 +77,9 @@ export class AutumnInt {
|
||||
this.headers["org-config"] = JSON.stringify(orgConfig);
|
||||
}
|
||||
|
||||
this.baseUrl = baseUrl || "http://localhost:8080/v1";
|
||||
this.baseUrl =
|
||||
baseUrl ||
|
||||
(liveUrl ? "https://api.useautumn.com/v1" : "http://localhost:8080/v1");
|
||||
}
|
||||
|
||||
async get(path: string) {
|
||||
@@ -91,13 +96,13 @@ export class AutumnInt {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (response.status != 200) {
|
||||
if (response.status !== 200) {
|
||||
let error: any;
|
||||
try {
|
||||
error = await response.json();
|
||||
} catch (error) {
|
||||
throw new AutumnError({
|
||||
message: "Failed to parse Autumn API error response",
|
||||
message: `AutumnInt post request failed, error: ${error}`,
|
||||
code: ErrCode.InternalError,
|
||||
});
|
||||
}
|
||||
@@ -127,13 +132,13 @@ export class AutumnInt {
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status != 200) {
|
||||
if (response.status !== 200) {
|
||||
let error: any;
|
||||
try {
|
||||
error = await response.json();
|
||||
} catch (error) {
|
||||
throw new AutumnError({
|
||||
message: "Failed to parse Autumn API error response",
|
||||
message: `AutumnInt delete request failed, error: ${error}`,
|
||||
code: ErrCode.InternalError,
|
||||
});
|
||||
}
|
||||
@@ -181,11 +186,6 @@ export class AutumnInt {
|
||||
async checkout(
|
||||
params: CheckoutParams & { invoice?: boolean; force_checkout?: boolean },
|
||||
) {
|
||||
// const data = await this.post(`/attach`, {
|
||||
// customer_id: customerId,
|
||||
// product_id: productId,
|
||||
// options: toSnakeCase(options),
|
||||
// });
|
||||
const data = await this.post(`/checkout`, params);
|
||||
|
||||
return data as CheckoutResult;
|
||||
@@ -249,6 +249,13 @@ export class AutumnInt {
|
||||
}
|
||||
|
||||
customers = {
|
||||
list: async (params?: { limit?: number; offset?: number }) => {
|
||||
const data = await this.get(
|
||||
`/customers?${new URLSearchParams(params as Record<string, string>).toString()}`,
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
get: async (
|
||||
customerId: string,
|
||||
params?: {
|
||||
|
||||
61
server/src/external/stripe/stripeSubUtils.ts
vendored
61
server/src/external/stripe/stripeSubUtils.ts
vendored
@@ -1,19 +1,21 @@
|
||||
import Stripe from "stripe";
|
||||
import {
|
||||
BillingInterval,
|
||||
type BillingInterval,
|
||||
CusProductStatus,
|
||||
Feature,
|
||||
FullCusProduct,
|
||||
Organization,
|
||||
type Feature,
|
||||
type FullCusProduct,
|
||||
type Organization,
|
||||
ProrationBehavior,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { differenceInSeconds } from "date-fns";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
// import { ProrationBehavior } from "@/internal/customers/change-product/handleUpgrade.js";
|
||||
import { SubService } from "@/internal/subscriptions/SubService.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { getEarliestPeriodEnd } from "./stripeSubUtils/convertSubUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import {
|
||||
getEarliestPeriodEnd,
|
||||
getLatestPeriodEnd,
|
||||
} from "./stripeSubUtils/convertSubUtils.js";
|
||||
|
||||
export const getFullStripeSub = async ({
|
||||
stripeCli,
|
||||
@@ -52,15 +54,23 @@ export const getStripeSubs = async ({
|
||||
}
|
||||
};
|
||||
|
||||
for (const subId of subIds) {
|
||||
const uniqueSubIds = Array.from(new Set(subIds));
|
||||
for (const subId of uniqueSubIds) {
|
||||
batchGet.push(getStripeSub(subId));
|
||||
}
|
||||
|
||||
let subs = await Promise.all(batchGet);
|
||||
subs = subs.filter((sub) => sub !== null);
|
||||
|
||||
// Sort by current_period_end (latest first)
|
||||
subs.sort((a: any, b: any) => {
|
||||
return b.current_period_end - a.current_period_end;
|
||||
subs.sort((a, b) => {
|
||||
if (!a || !b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const aLatestPeriodEnd = getLatestPeriodEnd({ sub: a });
|
||||
const bLatestPeriodEnd = getLatestPeriodEnd({ sub: b });
|
||||
return bLatestPeriodEnd - aLatestPeriodEnd;
|
||||
});
|
||||
|
||||
return subs as Stripe.Subscription[];
|
||||
@@ -120,9 +130,9 @@ export const getUsageBasedSub = async ({
|
||||
});
|
||||
}
|
||||
|
||||
let finalSubIds = subs.map((sub) => sub.id);
|
||||
const finalSubIds = subs.map((sub) => sub.id);
|
||||
|
||||
let autumnSubs = await SubService.getInStripeIds({
|
||||
const autumnSubs = await SubService.getInStripeIds({
|
||||
db,
|
||||
ids: finalSubIds,
|
||||
});
|
||||
@@ -131,9 +141,9 @@ export const getUsageBasedSub = async ({
|
||||
let usageFeatures: string[] | null = null;
|
||||
|
||||
// 1. Check if there's autumn sub
|
||||
let autumnSub = autumnSubs?.find((sub) => sub.stripe_id == stripeSub.id);
|
||||
const autumnSub = autumnSubs?.find((sub) => sub.stripe_id == stripeSub.id);
|
||||
if (autumnSub) {
|
||||
let containsFeature = autumnSub.usage_features.includes(
|
||||
const containsFeature = autumnSub.usage_features.includes(
|
||||
feature.internal_id!,
|
||||
);
|
||||
if (containsFeature) {
|
||||
@@ -169,26 +179,25 @@ export const getSubItemsForCusProduct = async ({
|
||||
stripeSub: Stripe.Subscription;
|
||||
cusProduct: FullCusProduct;
|
||||
}) => {
|
||||
let prices = cusProduct.customer_prices.map((cp) => cp.price);
|
||||
let product = cusProduct.product;
|
||||
const prices = cusProduct.customer_prices.map((cp) => cp.price);
|
||||
const product = cusProduct.product;
|
||||
|
||||
let subItems = [];
|
||||
const subItems: Stripe.SubscriptionItem[] = [];
|
||||
for (const item of stripeSub.items.data) {
|
||||
if (item.price.product == product.processor?.id) {
|
||||
if (item.price.product === product.processor?.id) {
|
||||
subItems.push(item);
|
||||
} else if (
|
||||
prices.some(
|
||||
(p) =>
|
||||
p.config?.stripe_price_id == item.price.id ||
|
||||
(p.config as UsagePriceConfig).stripe_product_id ==
|
||||
item.price.product,
|
||||
p.config.stripe_price_id === item.price.id ||
|
||||
p.config.stripe_product_id === item.price.product,
|
||||
)
|
||||
) {
|
||||
subItems.push(item);
|
||||
}
|
||||
}
|
||||
let otherSubItems = stripeSub.items.data.filter(
|
||||
(item) => !subItems.some((i) => i.id == item.id),
|
||||
const otherSubItems = stripeSub.items.data.filter(
|
||||
(item) => !subItems.some((i) => i.id === item.id),
|
||||
);
|
||||
|
||||
return { subItems, otherSubItems };
|
||||
@@ -244,7 +253,7 @@ export const getStripeSchedules = async ({
|
||||
batchGet.push(getStripeSchedule(scheduleId));
|
||||
}
|
||||
|
||||
let schedulesAndSubs = await Promise.all(batchGet);
|
||||
const schedulesAndSubs = await Promise.all(batchGet);
|
||||
|
||||
return schedulesAndSubs.filter((schedule) => schedule !== null) as {
|
||||
schedule: Stripe.SubscriptionSchedule;
|
||||
@@ -287,7 +296,7 @@ export const getStripeProrationBehavior = ({
|
||||
org: Organization;
|
||||
prorationBehavior?: ProrationBehavior;
|
||||
}) => {
|
||||
let behaviourMap = {
|
||||
const behaviourMap = {
|
||||
[ProrationBehavior.Immediately]: "always_invoice",
|
||||
[ProrationBehavior.NextBilling]: "create_prorations",
|
||||
[ProrationBehavior.None]: "none",
|
||||
|
||||
@@ -1,42 +1,41 @@
|
||||
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import {
|
||||
getPriceEntitlement,
|
||||
getEntOptions,
|
||||
getProductForPrice,
|
||||
} from "@/internal/products/prices/priceUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import {
|
||||
FullProduct,
|
||||
Price,
|
||||
EntitlementWithFeature,
|
||||
FeatureOptions,
|
||||
Organization,
|
||||
FullCusProduct,
|
||||
type APIVersion,
|
||||
type AttachConfig,
|
||||
type AttachReplaceable,
|
||||
BillingInterval,
|
||||
Entity,
|
||||
APIVersion,
|
||||
InsertReplaceable,
|
||||
AttachReplaceable,
|
||||
type EntitlementWithFeature,
|
||||
type Entity,
|
||||
ErrCode,
|
||||
AttachConfig,
|
||||
ProductOptions,
|
||||
type FeatureOptions,
|
||||
type FullCusProduct,
|
||||
type FullProduct,
|
||||
isUsagePrice,
|
||||
type Organization,
|
||||
type Price,
|
||||
type ProductOptions,
|
||||
} from "@autumn/shared";
|
||||
import { priceToStripeItem } from "../priceToStripeItem/priceToStripeItem.js";
|
||||
import { getArrearItems } from "./getStripeSubItems/getArrearItems.js";
|
||||
import {
|
||||
compareBillingIntervals,
|
||||
sortPricesByInterval,
|
||||
} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import {
|
||||
intervalKeyToPrice,
|
||||
priceToIntervalKey,
|
||||
priceToProductOptions,
|
||||
} from "@/internal/products/prices/priceUtils/convertPrice.js";
|
||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { ItemSet } from "@/utils/models/ItemSet.js";
|
||||
import {
|
||||
compareBillingIntervals,
|
||||
sortPricesByInterval,
|
||||
} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import {
|
||||
getEntOptions,
|
||||
getPriceEntitlement,
|
||||
getProductForPrice,
|
||||
} from "@/internal/products/prices/priceUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import type { ItemSet } from "@/utils/models/ItemSet.js";
|
||||
import { priceToStripeItem } from "../priceToStripeItem/priceToStripeItem.js";
|
||||
import { getArrearItems } from "./getStripeSubItems/getArrearItems.js";
|
||||
|
||||
const getIntervalToPrices = (prices: Price[]) => {
|
||||
const intervalToPrices: Record<string, Price[]> = {};
|
||||
@@ -50,14 +49,14 @@ const getIntervalToPrices = (prices: Price[]) => {
|
||||
intervalToPrices[key].push(price);
|
||||
}
|
||||
|
||||
let oneOffPrices =
|
||||
const oneOffPrices =
|
||||
intervalToPrices[BillingInterval.OneOff] &&
|
||||
intervalToPrices[BillingInterval.OneOff].length > 0;
|
||||
|
||||
// If there are multiple intervals, add one off prices to first interval
|
||||
if (oneOffPrices && Object.keys(intervalToPrices).length > 1) {
|
||||
const nextIntervalKey = Object.keys(intervalToPrices)[0];
|
||||
intervalToPrices[nextIntervalKey!].push(
|
||||
intervalToPrices[nextIntervalKey].push(
|
||||
...structuredClone(intervalToPrices[BillingInterval.OneOff]),
|
||||
);
|
||||
delete intervalToPrices[BillingInterval.OneOff];
|
||||
@@ -107,8 +106,8 @@ export const getStripeSubItems = async ({
|
||||
for (const intervalKey in intervalToPrices) {
|
||||
const prices = intervalToPrices[intervalKey];
|
||||
|
||||
let subItems: any[] = [];
|
||||
let usage_features: any[] = [];
|
||||
const subItems: any[] = [];
|
||||
const usage_features: any[] = [];
|
||||
|
||||
for (const price of prices) {
|
||||
const prodOptions = priceToProductOptions({
|
||||
@@ -128,7 +127,7 @@ export const getStripeSubItems = async ({
|
||||
internalEntityId,
|
||||
});
|
||||
|
||||
let replaceables = priceEnt
|
||||
const replaceables = priceEnt
|
||||
? attachParams.replaceables.filter((r) => r.ent.id === priceEnt.id)
|
||||
: [];
|
||||
|
||||
@@ -141,7 +140,7 @@ export const getStripeSubItems = async ({
|
||||
});
|
||||
}
|
||||
|
||||
let product = getProductForPrice(price, products)!;
|
||||
const product = getProductForPrice(price, products);
|
||||
|
||||
if (!product) {
|
||||
logger.error(
|
||||
@@ -183,7 +182,7 @@ export const getStripeSubItems = async ({
|
||||
}
|
||||
|
||||
const { interval, intervalCount } = intervalKeyToPrice(intervalKey);
|
||||
if (subItems.length == 0) {
|
||||
if (subItems.length === 0) {
|
||||
subItems.push(
|
||||
...getArrearItems({
|
||||
prices,
|
||||
@@ -260,13 +259,13 @@ export const getStripeSubItems2 = async ({
|
||||
internalEntityId,
|
||||
});
|
||||
|
||||
let replaceables = priceEnt
|
||||
const replaceables = priceEnt
|
||||
? attachParams.replaceables.filter((r) => r.ent.id === priceEnt.id)
|
||||
: [];
|
||||
|
||||
existingUsage += replaceables.length;
|
||||
|
||||
let product = getProductForPrice(price, attachParams.products)!;
|
||||
const product = getProductForPrice(price, attachParams.products)!;
|
||||
|
||||
if (!product) {
|
||||
logger.error(
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { cusProductToPrices } from "@autumn/shared";
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import {
|
||||
BillingType,
|
||||
FullCusProduct,
|
||||
Organization,
|
||||
Price,
|
||||
prices,
|
||||
type BillingType,
|
||||
cusProductToPrices,
|
||||
type FullCusProduct,
|
||||
type Price,
|
||||
PriceType,
|
||||
UsagePriceConfig,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import type Stripe from "stripe";
|
||||
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
|
||||
const autumnStripePricesMatch = ({
|
||||
stripePrice,
|
||||
@@ -24,16 +22,16 @@ const autumnStripePricesMatch = ({
|
||||
}) => {
|
||||
const config = autumnPrice.config as UsagePriceConfig;
|
||||
|
||||
if (config.type == PriceType.Fixed) {
|
||||
if (config.type === PriceType.Fixed) {
|
||||
return (
|
||||
config.stripe_price_id == stripePrice.id ||
|
||||
(stripeProdId && stripePrice.product == stripeProdId)
|
||||
config.stripe_price_id === stripePrice.id ||
|
||||
(stripeProdId && stripePrice.product === stripeProdId)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
config.stripe_price_id == stripePrice.id ||
|
||||
config.stripe_product_id == stripePrice.product ||
|
||||
config.stripe_empty_price_id == stripePrice.id
|
||||
config.stripe_price_id === stripePrice.id ||
|
||||
config.stripe_product_id === stripePrice.product ||
|
||||
config.stripe_empty_price_id === stripePrice.id
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -79,7 +77,7 @@ export const findStripeItemForPrice = ({
|
||||
}) => {
|
||||
if (invoiceLineItems) {
|
||||
return invoiceLineItems.find((li) => {
|
||||
return li.pricing?.price_details?.price == price.config.stripe_price_id;
|
||||
return li.pricing?.price_details?.price === price.config.stripe_price_id;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,9 +87,9 @@ export const findStripeItemForPrice = ({
|
||||
const config = price.config as UsagePriceConfig;
|
||||
|
||||
return (
|
||||
config.stripe_price_id == si.price?.id ||
|
||||
config.stripe_product_id == si.price?.product ||
|
||||
config.stripe_empty_price_id == si.price?.id
|
||||
config.stripe_price_id === si.price?.id ||
|
||||
config.stripe_product_id === si.price?.product ||
|
||||
config.stripe_empty_price_id === si.price?.id
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -105,31 +103,14 @@ export const findStripeItemForPrice = ({
|
||||
const config = price.config as UsagePriceConfig;
|
||||
|
||||
return (
|
||||
config.stripe_price_id == si.price?.id ||
|
||||
(stripeProdId && si.price?.product == stripeProdId)
|
||||
config.stripe_price_id === si.price?.id ||
|
||||
(stripeProdId && si.price?.product === stripeProdId)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
// return stripeItems.find((si: Stripe.SubscriptionItem | Stripe.LineItem) => {
|
||||
// const config = price.config as UsagePriceConfig;
|
||||
|
||||
// if (config.type == PriceType.Fixed) {
|
||||
// return (
|
||||
// config.stripe_price_id == si.price?.id ||
|
||||
// (stripeProdId && si.price?.product == stripeProdId)
|
||||
// );
|
||||
// } else {
|
||||
// return (
|
||||
// config.stripe_price_id == si.price?.id ||
|
||||
// config.stripe_product_id == si.price?.product ||
|
||||
// config.stripe_empty_price_id == si.price?.id
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,24 +126,26 @@ export const findPriceInStripeItems = ({
|
||||
billingType?: BillingType;
|
||||
}) => {
|
||||
return prices.find((p: Price) => {
|
||||
let config = p.config;
|
||||
const config = p.config;
|
||||
|
||||
let itemMatch;
|
||||
let itemMatch: boolean = false;
|
||||
if (subItem) {
|
||||
itemMatch =
|
||||
config.stripe_price_id == subItem.price?.id ||
|
||||
config.stripe_product_id == subItem.price?.product;
|
||||
config.stripe_price_id === subItem.price?.id ||
|
||||
config.stripe_product_id === subItem.price?.product;
|
||||
}
|
||||
|
||||
if (lineItem) {
|
||||
const priceDetails = lineItem.pricing?.price_details;
|
||||
itemMatch =
|
||||
config.stripe_price_id == priceDetails?.price ||
|
||||
config.stripe_product_id == priceDetails?.product;
|
||||
config.stripe_price_id === priceDetails?.price ||
|
||||
config.stripe_product_id === priceDetails?.product;
|
||||
}
|
||||
|
||||
const priceBillingType = getBillingType(config);
|
||||
let billingTypeMatch = billingType ? priceBillingType == billingType : true;
|
||||
const billingTypeMatch = billingType
|
||||
? priceBillingType === billingType
|
||||
: true;
|
||||
|
||||
return itemMatch && billingTypeMatch;
|
||||
});
|
||||
@@ -190,14 +173,14 @@ export const lineItemInCusProduct = ({
|
||||
cusProduct: FullCusProduct;
|
||||
lineItem: Stripe.InvoiceLineItem;
|
||||
}) => {
|
||||
let stripeProdId = cusProduct.product.processor?.id;
|
||||
const stripeProdId = cusProduct.product.processor?.id;
|
||||
|
||||
let prices = cusProductToPrices({ cusProduct });
|
||||
let price = findPriceInStripeItems({ prices, lineItem });
|
||||
const prices = cusProductToPrices({ cusProduct });
|
||||
const price = findPriceInStripeItems({ prices, lineItem });
|
||||
|
||||
const priceDetails = lineItem.pricing?.price_details;
|
||||
|
||||
return stripeProdId == priceDetails?.product || notNullish(price);
|
||||
return stripeProdId === priceDetails?.product || notNullish(price);
|
||||
};
|
||||
|
||||
export const subItemInCusProduct = ({
|
||||
@@ -207,12 +190,12 @@ export const subItemInCusProduct = ({
|
||||
cusProduct: FullCusProduct;
|
||||
subItem: Stripe.SubscriptionItem;
|
||||
}) => {
|
||||
let stripeProdId = cusProduct.product.processor?.id;
|
||||
const stripeProdId = cusProduct.product.processor?.id;
|
||||
|
||||
let prices = cusProductToPrices({ cusProduct });
|
||||
let price = findPriceInStripeItems({ prices, subItem });
|
||||
const prices = cusProductToPrices({ cusProduct });
|
||||
const price = findPriceInStripeItems({ prices, subItem });
|
||||
|
||||
return stripeProdId == subItem.price.product || notNullish(price);
|
||||
return stripeProdId === subItem.price.product || notNullish(price);
|
||||
};
|
||||
|
||||
export const scheduleItemToPrice = ({
|
||||
@@ -247,10 +230,10 @@ export const scheduleItemInCusProduct = ({
|
||||
cusProduct: FullCusProduct;
|
||||
scheduleItem: Stripe.SubscriptionSchedule.Phase.Item;
|
||||
}) => {
|
||||
let stripeProdId = cusProduct.product.processor?.id;
|
||||
const stripeProdId = cusProduct.product.processor?.id;
|
||||
|
||||
let autumnPrices = cusProductToPrices({ cusProduct });
|
||||
let price = autumnPrices.find((p) => {
|
||||
const autumnPrices = cusProductToPrices({ cusProduct });
|
||||
const price = autumnPrices.find((p) => {
|
||||
const stripePrice = scheduleItem.price as Stripe.Price;
|
||||
|
||||
return autumnStripePricesMatch({
|
||||
@@ -268,7 +251,7 @@ export const isLicenseItem = ({
|
||||
}: {
|
||||
stripeItem: Stripe.SubscriptionItem | Stripe.LineItem;
|
||||
}) => {
|
||||
return stripeItem.price?.recurring?.usage_type == "licensed";
|
||||
return stripeItem.price?.recurring?.usage_type === "licensed";
|
||||
};
|
||||
|
||||
export const isMeteredItem = ({
|
||||
@@ -276,7 +259,7 @@ export const isMeteredItem = ({
|
||||
}: {
|
||||
stripeItem: Stripe.SubscriptionItem | Stripe.LineItem;
|
||||
}) => {
|
||||
return stripeItem.price?.recurring?.usage_type == "metered";
|
||||
return stripeItem.price?.recurring?.usage_type === "metered";
|
||||
};
|
||||
|
||||
// Get sub item from product
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { APIVersion, Organization, UsagePriceConfig } from "@autumn/shared";
|
||||
import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { APIVersion, isUsagePrice, type Organization } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { getEmptyPriceItem } from "../../priceToStripeItem/priceToStripeItem.js";
|
||||
|
||||
export const handleRemainingSets = async ({
|
||||
@@ -23,10 +22,10 @@ export const handleRemainingSets = async ({
|
||||
logger: any;
|
||||
}) => {
|
||||
const itemSets = attachParams.itemSets;
|
||||
let remainingSets = itemSets ? itemSets.slice(1) : [];
|
||||
const remainingSets = itemSets ? itemSets.slice(1) : [];
|
||||
|
||||
const remainingItems = remainingSets.flatMap((set) => set.items);
|
||||
let invoiceIds: string[] = checkoutSession.invoice
|
||||
const invoiceIds: string[] = checkoutSession.invoice
|
||||
? [checkoutSession.invoice as string]
|
||||
: [];
|
||||
|
||||
@@ -34,18 +33,18 @@ export const handleRemainingSets = async ({
|
||||
for (const price of attachParams.prices) {
|
||||
if (!isUsagePrice({ price })) continue;
|
||||
|
||||
const config = price.config as UsagePriceConfig;
|
||||
const config = price.config;
|
||||
const emptyPrice = config.stripe_empty_price_id;
|
||||
|
||||
if (
|
||||
attachParams.internalEntityId ||
|
||||
attachParams.apiVersion == APIVersion.v1_4
|
||||
attachParams.apiVersion === APIVersion.v1_4
|
||||
) {
|
||||
const replaceIndex = remainingItems.findIndex(
|
||||
(item) => item.price == config.stripe_price_id,
|
||||
(item) => item.price === config.stripe_price_id,
|
||||
);
|
||||
|
||||
if (replaceIndex != -1) {
|
||||
if (replaceIndex !== -1) {
|
||||
remainingItems[replaceIndex] = emptyPrice
|
||||
? {
|
||||
price: config.stripe_empty_price_id,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ErrCode, type FullCustomer } from "@autumn/shared";
|
||||
import type { ClickHouseClient } from "@clickhouse/client";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
@@ -302,7 +303,7 @@ order by dr.period;
|
||||
resultJson.data.forEach((row: any) => {
|
||||
Object.keys(row).forEach((key: string) => {
|
||||
if (key !== "period") {
|
||||
row[key] = parseInt(row[key]);
|
||||
row[key] = new Decimal(row[key]).toDecimalPlaces(10).toNumber();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import {
|
||||
ErrCode,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
} from "@autumn/shared";
|
||||
import { Router } from "express";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { CacheType } from "@/external/caching/cacheActions.js";
|
||||
import { queryWithCache } from "@/external/caching/cacheUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { CusService } from "../customers/CusService.js";
|
||||
import { AnalyticsService } from "./AnalyticsService.js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import {
|
||||
AppEnv,
|
||||
ErrCode,
|
||||
Feature,
|
||||
FeatureType,
|
||||
FullCusProduct,
|
||||
FullCustomer,
|
||||
Organization,
|
||||
} from "@autumn/shared";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { queryWithCache } from "@/external/caching/cacheUtils.js";
|
||||
import { CacheType } from "@/external/caching/cacheActions.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
|
||||
export const analyticsRouter = Router();
|
||||
|
||||
@@ -28,7 +26,7 @@ analyticsRouter.get("/event_names", async (req: any, res: any) =>
|
||||
const { db, org, env, features } = req;
|
||||
const { interval, event_names, customer_id } = req.body;
|
||||
|
||||
let result = await queryWithCache({
|
||||
const result = await queryWithCache({
|
||||
action: CacheType.TopEvents,
|
||||
key: `${org.id}_${env}`,
|
||||
fn: async () => {
|
||||
@@ -46,15 +44,15 @@ analyticsRouter.get("/event_names", async (req: any, res: any) =>
|
||||
|
||||
// let result = topEventNamesRes?.eventNames;
|
||||
|
||||
let featureIds: string[] = [];
|
||||
let eventNames: string[] = [];
|
||||
const featureIds: string[] = [];
|
||||
const eventNames: string[] = [];
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
// Is an event name
|
||||
if (
|
||||
features.some(
|
||||
(feature: Feature) =>
|
||||
feature.type == FeatureType.Metered &&
|
||||
feature.type === FeatureType.Metered &&
|
||||
feature.config.filters?.[0]?.value.includes(result[i]),
|
||||
)
|
||||
) {
|
||||
@@ -95,17 +93,17 @@ const getTopEvents = async ({ req }: { req: ExtendedRequest }) => {
|
||||
req,
|
||||
});
|
||||
|
||||
let result = topEventNamesRes?.eventNames;
|
||||
const result = topEventNamesRes?.eventNames;
|
||||
|
||||
let featureIds: string[] = [];
|
||||
let eventNames: string[] = [];
|
||||
const featureIds: string[] = [];
|
||||
const eventNames: string[] = [];
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
// Is an event name
|
||||
if (
|
||||
features.some(
|
||||
(feature: Feature) =>
|
||||
feature.type == FeatureType.Metered &&
|
||||
feature.type === FeatureType.Metered &&
|
||||
feature.config.filters?.[0]?.value.includes(result[i]),
|
||||
)
|
||||
) {
|
||||
@@ -131,9 +129,7 @@ analyticsRouter.post("/events", async (req: any, res: any) =>
|
||||
const { db, org, env, features } = req;
|
||||
let { interval, event_names, customer_id } = req.body;
|
||||
|
||||
let topEvents:
|
||||
| { featureIds: string[]; eventNames: string[] }
|
||||
| undefined = undefined;
|
||||
let topEvents: { featureIds: string[]; eventNames: string[] } | undefined;
|
||||
|
||||
if (!event_names || event_names.length === 0) {
|
||||
topEvents = await getTopEvents({ req });
|
||||
@@ -141,7 +137,7 @@ analyticsRouter.post("/events", async (req: any, res: any) =>
|
||||
}
|
||||
|
||||
let aggregateAll = false;
|
||||
let customer: FullCustomer | undefined = undefined;
|
||||
let customer: FullCustomer | undefined;
|
||||
let bcExclusionFlag = false;
|
||||
|
||||
if (!customer_id) {
|
||||
@@ -190,8 +186,6 @@ analyticsRouter.post("/events", async (req: any, res: any) =>
|
||||
aggregateAll,
|
||||
});
|
||||
|
||||
// console.log("events", events);
|
||||
|
||||
res.status(200).json({
|
||||
customer,
|
||||
events,
|
||||
@@ -214,7 +208,7 @@ analyticsRouter.post("/raw", async (req: any, res: any) =>
|
||||
const { interval, customer_id } = req.body;
|
||||
|
||||
let aggregateAll = false;
|
||||
let customer: FullCustomer | undefined = undefined;
|
||||
let customer: FullCustomer | undefined;
|
||||
|
||||
if (!customer_id) {
|
||||
// No customer ID provided, set aggregateAll to true
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { fullCusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import {
|
||||
isOneOff,
|
||||
isProductUpgrade,
|
||||
} from "@/internal/products/productUtils.js";
|
||||
import { sortProductsByPrice } from "@/internal/products/productUtils/sortProductUtils.js";
|
||||
import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import {
|
||||
type Feature,
|
||||
FeaturePreviewScenario,
|
||||
type FullCusProduct,
|
||||
type FullEntitlement,
|
||||
type FullProduct,
|
||||
} from "@autumn/shared";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { fullCusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
|
||||
import {
|
||||
sortFullProducts,
|
||||
sortProductsByPrice,
|
||||
} from "@/internal/products/productUtils/sortProductUtils.js";
|
||||
import {
|
||||
isOneOff,
|
||||
isProductUpgrade,
|
||||
} from "@/internal/products/productUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
|
||||
export const getCheckPreview = async ({
|
||||
db,
|
||||
@@ -34,30 +38,34 @@ export const getCheckPreview = async ({
|
||||
return null;
|
||||
}
|
||||
|
||||
let mainCusProds = cusProducts.filter(
|
||||
const mainCusProds = cusProducts.filter(
|
||||
(cp: FullCusProduct) => !cp.product.is_add_on,
|
||||
);
|
||||
|
||||
let cusOwnedProducts = mainCusProds.map((cp: FullCusProduct) =>
|
||||
const cusOwnedProducts = mainCusProds.map((cp: FullCusProduct) =>
|
||||
fullCusProductToProduct(cp),
|
||||
);
|
||||
|
||||
sortProductsByPrice({ products: cusOwnedProducts });
|
||||
let highestTierProd =
|
||||
|
||||
const highestTierProd =
|
||||
cusOwnedProducts.length > 0 ? cusOwnedProducts[0] : null;
|
||||
|
||||
let products: FullProduct[] = await ProductService.getByFeature({
|
||||
const products: FullProduct[] = await ProductService.getByFeature({
|
||||
db,
|
||||
internalFeatureId: feature.internal_id!,
|
||||
internalFeatureId: feature.internal_id,
|
||||
});
|
||||
|
||||
sortFullProducts({ products });
|
||||
|
||||
// 1. Get add ons
|
||||
let addOns = [];
|
||||
for (let addOn of products) {
|
||||
const addOns = [];
|
||||
for (const addOn of products) {
|
||||
if (addOn.is_add_on) {
|
||||
if (isOneOff(addOn.prices)) {
|
||||
addOns.push(addOn);
|
||||
} else if (
|
||||
!cusProducts.some((cp: FullCusProduct) => cp.product.id == addOn.id)
|
||||
!cusProducts.some((cp: FullCusProduct) => cp.product.id === addOn.id)
|
||||
) {
|
||||
addOns.push(addOn);
|
||||
}
|
||||
@@ -68,12 +76,13 @@ export const getCheckPreview = async ({
|
||||
if (!highestTierProd) {
|
||||
mainProds = products.filter((product: FullProduct) => !product.is_add_on);
|
||||
} else {
|
||||
for (let prod of products) {
|
||||
for (const prod of products) {
|
||||
if (prod.is_add_on) {
|
||||
continue;
|
||||
}
|
||||
if (mainCusProds.some((cp: FullCusProduct) => cp.product.id == prod.id)) {
|
||||
continue;
|
||||
if (
|
||||
mainCusProds.some((cp: FullCusProduct) => cp.product.id === prod.id)
|
||||
) {
|
||||
} else if (
|
||||
isProductUpgrade({
|
||||
prices1: highestTierProd.prices,
|
||||
@@ -86,21 +95,21 @@ export const getCheckPreview = async ({
|
||||
}
|
||||
}
|
||||
|
||||
let rawProducts = [...mainProds, ...addOns];
|
||||
for (let p of rawProducts) {
|
||||
const rawProducts = [...mainProds, ...addOns];
|
||||
for (const p of rawProducts) {
|
||||
p.entitlements = p.entitlements.map((e) => ({
|
||||
...e,
|
||||
feature: allFeatures.find((f) => f.id == e.feature_id)!,
|
||||
}));
|
||||
feature: allFeatures.find((f) => f.id === e.feature_id),
|
||||
})) as FullEntitlement[];
|
||||
}
|
||||
|
||||
let v2Prods = await Promise.all(
|
||||
const v2Prods = await Promise.all(
|
||||
rawProducts.map((p) =>
|
||||
getProductResponse({ product: p, features: allFeatures }),
|
||||
),
|
||||
);
|
||||
|
||||
let scenario = notNullish(balance)
|
||||
const scenario = notNullish(balance)
|
||||
? FeaturePreviewScenario.UsageLimit
|
||||
: FeaturePreviewScenario.FeatureFlag;
|
||||
|
||||
@@ -111,7 +120,7 @@ export const getCheckPreview = async ({
|
||||
feature_id: feature.id,
|
||||
feature_name: feature.name,
|
||||
message:
|
||||
scenario == FeaturePreviewScenario.UsageLimit
|
||||
scenario === FeaturePreviewScenario.UsageLimit
|
||||
? `You have reached the usage limit for ${feature.name}. Please contact us to increase your limit.`
|
||||
: `${feature.name} is not available for your account. Please contact us to enable it.`,
|
||||
|
||||
@@ -120,9 +129,9 @@ export const getCheckPreview = async ({
|
||||
};
|
||||
}
|
||||
|
||||
let nextProd = mainProds.length > 0 ? mainProds[0] : addOns[0];
|
||||
const nextProd = mainProds.length > 0 ? mainProds[0] : addOns[0];
|
||||
|
||||
let title = nextProd.free_trial
|
||||
const title = nextProd.free_trial
|
||||
? `Start trial for ${nextProd.name}`
|
||||
: !nextProd.is_add_on
|
||||
? `Upgrade to ${nextProd.name}`
|
||||
@@ -140,7 +149,7 @@ export const getCheckPreview = async ({
|
||||
}
|
||||
msg = `${msg} ${prodString}`;
|
||||
} else if (addOns.length > 0) {
|
||||
let prodString = `Please purchase the ${addOns[0].name} add on to continue using this feature.`;
|
||||
const prodString = `Please purchase the ${addOns[0].name} add on to continue using this feature.`;
|
||||
msg = `${msg} ${prodString}`;
|
||||
}
|
||||
}
|
||||
@@ -155,12 +164,12 @@ export const getCheckPreview = async ({
|
||||
}
|
||||
msg = `${msg} ${prodString}`;
|
||||
} else if (addOns.length > 0) {
|
||||
let prodString = `Please purchase the ${addOns[0].name} add on to use this feature.`;
|
||||
const prodString = `Please purchase the ${addOns[0].name} add on to use this feature.`;
|
||||
msg = `${msg} ${prodString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let nextTier =
|
||||
const nextTier =
|
||||
mainProds.length > 0 ? mainProds[0] : addOns.length > 0 ? addOns[0] : null;
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import {
|
||||
ErrCode,
|
||||
nullish,
|
||||
type RewardProgram,
|
||||
RewardTriggerEvent,
|
||||
UpdateRewardProgram,
|
||||
} from "@autumn/shared";
|
||||
import express, { type Router } from "express";
|
||||
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import {
|
||||
handleCreateRewardProgram,
|
||||
handleDeleteRewardProgram,
|
||||
} from "./handlers/rewardPrograms/index.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
CreateRewardProgram,
|
||||
ErrCode,
|
||||
nullish,
|
||||
RewardTriggerEvent,
|
||||
} from "@autumn/shared";
|
||||
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
|
||||
import { constructRewardProgram } from "@/internal/rewards/rewardTriggerUtils.js";
|
||||
|
||||
export const rewardProgramRouter: Router = express.Router();
|
||||
|
||||
@@ -39,7 +39,7 @@ rewardProgramRouter.put("/:id", (req, res) =>
|
||||
}
|
||||
|
||||
// Ensure program exists
|
||||
let existingProgram = await RewardProgramService.get({
|
||||
const existingProgram = await RewardProgramService.get({
|
||||
db,
|
||||
idOrInternalId: id,
|
||||
orgId,
|
||||
@@ -54,21 +54,22 @@ rewardProgramRouter.put("/:id", (req, res) =>
|
||||
});
|
||||
}
|
||||
|
||||
const rewardProgram = constructRewardProgram({
|
||||
rewardProgramData: CreateRewardProgram.parse({
|
||||
...body,
|
||||
id: existingProgram.id, // ID cannot be changed
|
||||
}),
|
||||
orgId,
|
||||
env,
|
||||
});
|
||||
// const rewardProgram = constructRewardProgram({
|
||||
// rewardProgramData: CreateRewardProgram.parse({
|
||||
// ...body,
|
||||
// id: existingProgram.id, // ID cannot be changed
|
||||
// }),
|
||||
// orgId,
|
||||
// env,
|
||||
// });
|
||||
|
||||
// Update on existing redemptions? (should be none unless affecting stacked rewards...)
|
||||
|
||||
const data = UpdateRewardProgram.parse(body);
|
||||
|
||||
if (
|
||||
rewardProgram.when == RewardTriggerEvent.Checkout &&
|
||||
(nullish(rewardProgram.product_ids) ||
|
||||
rewardProgram.product_ids!.length == 0)
|
||||
data.when === RewardTriggerEvent.Checkout &&
|
||||
(nullish(data.product_ids) || data.product_ids.length === 0)
|
||||
) {
|
||||
throw new RecaseError({
|
||||
message:
|
||||
@@ -78,12 +79,12 @@ rewardProgramRouter.put("/:id", (req, res) =>
|
||||
});
|
||||
}
|
||||
|
||||
let updatedRewardProgram = await RewardProgramService.update({
|
||||
const updatedRewardProgram = await RewardProgramService.update({
|
||||
db,
|
||||
idOrInternalId: id,
|
||||
orgId,
|
||||
env,
|
||||
data: rewardProgram,
|
||||
data: data as RewardProgram,
|
||||
});
|
||||
|
||||
return res.status(200).json(updatedRewardProgram);
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
attachParamsToCurCusProduct,
|
||||
getCustomerSchedule,
|
||||
getCustomerSub,
|
||||
paramsToCurSubSchedule,
|
||||
} from "../../attachUtils/convertAttachParams.js";
|
||||
import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js";
|
||||
import { getCurrentPhaseIndex } from "../../mergeUtils/phaseUtils/phaseUtils.js";
|
||||
@@ -54,7 +54,12 @@ export const handleScheduleFunction2 = async ({
|
||||
const { sub: curSub } = await getCustomerSub({ attachParams });
|
||||
|
||||
// 1. Cancel current subscription and fetch items from other cus products...?
|
||||
let schedule = await paramsToCurSubSchedule({ attachParams });
|
||||
let { schedule } = await getCustomerSchedule({
|
||||
attachParams,
|
||||
subId: curSub?.id,
|
||||
logger,
|
||||
});
|
||||
|
||||
if (!curSub) {
|
||||
throw new RecaseError({
|
||||
message: `SCHEDULE FLOW, curSub is undefined`,
|
||||
|
||||
@@ -226,8 +226,8 @@ export const paramsToCurSub = async ({
|
||||
}) => {
|
||||
const { stripeCli } = attachParams;
|
||||
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
|
||||
console.log("Cur cus product:", curCusProduct);
|
||||
console.log("Sub IDs:", curCusProduct?.subscription_ids);
|
||||
// console.log("Cur cus product:", curCusProduct);
|
||||
// console.log("Sub IDs:", curCusProduct?.subscription_ids);
|
||||
|
||||
const subIds = curCusProduct?.subscription_ids || [];
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import {
|
||||
isArrearPrice,
|
||||
isContUsePrice,
|
||||
type AttachBody,
|
||||
type AttachBranch,
|
||||
isUsagePrice,
|
||||
} from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
AttachBody,
|
||||
AttachBranch,
|
||||
notNullish,
|
||||
nullish,
|
||||
Price,
|
||||
type Price,
|
||||
} from "@autumn/shared";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
|
||||
export const handleMultiAttachErrors = async ({
|
||||
attachParams,
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
import { AttachFunction, FeatureOptions } from "@autumn/shared";
|
||||
|
||||
import {
|
||||
type AttachBody,
|
||||
AttachBodySchema,
|
||||
AttachFunction,
|
||||
type FeatureOptions,
|
||||
} from "@autumn/shared";
|
||||
import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js";
|
||||
import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { getPriceOptions } from "@/internal/products/prices/priceUtils.js";
|
||||
import type {
|
||||
ExtendedRequest,
|
||||
ExtendedResponse,
|
||||
} from "@/utils/models/Request.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js";
|
||||
import { AttachBody, AttachBodySchema } from "@autumn/shared";
|
||||
import { ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
|
||||
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
|
||||
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
|
||||
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
|
||||
import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js";
|
||||
import type { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import {
|
||||
checkStripeConnections,
|
||||
handlePrepaidErrors,
|
||||
} from "../attachRouter.js";
|
||||
import { attachParamsToPreview } from "../handleAttachPreview/attachParamsToPreview.js";
|
||||
import { previewToCheckoutRes } from "./previewToCheckoutRes.js";
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js";
|
||||
import { attachParamsToProduct } from "../attachUtils/convertAttachParams.js";
|
||||
import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js";
|
||||
import { getPriceOptions } from "@/internal/products/prices/priceUtils.js";
|
||||
import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
|
||||
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
|
||||
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
|
||||
import { attachParamsToPreview } from "../handleAttachPreview/attachParamsToPreview.js";
|
||||
import { getHasProrations } from "./getHasProrations.js";
|
||||
import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js";
|
||||
import { z } from "zod";
|
||||
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
|
||||
import { previewToCheckoutRes } from "./previewToCheckoutRes.js";
|
||||
|
||||
const getAttachVars = async ({
|
||||
req,
|
||||
@@ -79,13 +82,15 @@ const getCheckoutOptions = async ({
|
||||
isPrepaidPrice({ price: p }),
|
||||
);
|
||||
|
||||
let newOptions: FeatureOptions[] = structuredClone(attachParams.optionsList);
|
||||
const newOptions: FeatureOptions[] = structuredClone(
|
||||
attachParams.optionsList,
|
||||
);
|
||||
for (const prepaidPrice of prepaidPrices) {
|
||||
const feature = priceToFeature({
|
||||
price: prepaidPrice,
|
||||
features: req.features,
|
||||
});
|
||||
let option = getPriceOptions(prepaidPrice, attachParams.optionsList);
|
||||
const option = getPriceOptions(prepaidPrice, attachParams.optionsList);
|
||||
if (!option) {
|
||||
newOptions.push({
|
||||
feature_id: feature?.id!,
|
||||
@@ -116,7 +121,7 @@ export const handleCheckout = (req: any, res: any) =>
|
||||
|
||||
let checkoutUrl = null;
|
||||
|
||||
if (func == AttachFunction.CreateCheckout) {
|
||||
if (func === AttachFunction.CreateCheckout) {
|
||||
await checkStripeConnections({
|
||||
req,
|
||||
attachParams,
|
||||
@@ -182,17 +187,9 @@ export const handleCheckout = (req: any, res: any) =>
|
||||
attachParams,
|
||||
});
|
||||
|
||||
if (checkoutRes.next_cycle) {
|
||||
const nextCycle = checkoutRes.next_cycle;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
...checkoutRes,
|
||||
url: checkoutUrl,
|
||||
options: attachParams.optionsList.map((o) => ({
|
||||
quantity: o.quantity,
|
||||
feature_id: o.feature_id,
|
||||
})),
|
||||
has_prorations: hasProrations,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import {
|
||||
AttachBranch,
|
||||
AttachPreview,
|
||||
CheckoutLine,
|
||||
type AttachBranch,
|
||||
type AttachPreview,
|
||||
type CheckoutLine,
|
||||
CheckoutResponseSchema,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import {
|
||||
attachParamsToProduct,
|
||||
attachParamToCusProducts,
|
||||
} from "../attachUtils/convertAttachParams.js";
|
||||
import {
|
||||
cusProductToEnts,
|
||||
cusProductToPrices,
|
||||
cusProductToProduct,
|
||||
isUsagePrice,
|
||||
type PreviewLineItem,
|
||||
toProductItem,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
|
||||
import { isPriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js";
|
||||
import {
|
||||
getProductItemResponse,
|
||||
getProductResponse,
|
||||
} from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
|
||||
import { toProductItem } from "@autumn/shared";
|
||||
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
|
||||
import { formatUnixToDateTime, notNullish } from "@/utils/genUtils.js";
|
||||
import { isPriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import type { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import {
|
||||
attachParamsToProduct,
|
||||
attachParamToCusProducts,
|
||||
} from "../attachUtils/convertAttachParams.js";
|
||||
|
||||
export const previewToCheckoutRes = async ({
|
||||
req,
|
||||
@@ -42,29 +41,29 @@ export const previewToCheckoutRes = async ({
|
||||
const product = attachParamsToProduct({ attachParams });
|
||||
|
||||
const { curCusProduct } = attachParamToCusProducts({ attachParams });
|
||||
let curPrices = curCusProduct
|
||||
const curPrices = curCusProduct
|
||||
? cusProductToPrices({ cusProduct: curCusProduct })
|
||||
: [];
|
||||
let curEnts = curCusProduct
|
||||
const curEnts = curCusProduct
|
||||
? cusProductToEnts({ cusProduct: curCusProduct })
|
||||
: [];
|
||||
|
||||
let newPrices = attachParams.prices;
|
||||
let newEnts = attachParams.entitlements;
|
||||
let allPrices = [...curPrices, ...newPrices];
|
||||
let allEnts = [...curEnts, ...newEnts];
|
||||
const newPrices = attachParams.prices;
|
||||
const newEnts = attachParams.entitlements;
|
||||
const allPrices = [...curPrices, ...newPrices];
|
||||
const allEnts = [...curEnts, ...newEnts];
|
||||
let lines: CheckoutLine[] = [];
|
||||
|
||||
if (preview.due_today && preview.due_today.line_items.length > 0) {
|
||||
lines = preview.due_today.line_items
|
||||
.map((li: any) => {
|
||||
let price = allPrices.find((p) => p.id == li.price_id);
|
||||
.map((li: PreviewLineItem) => {
|
||||
const price = allPrices.find((p) => p.id === li.price_id);
|
||||
|
||||
if (!price) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let ent = getPriceEntitlement(price, allEnts);
|
||||
const ent = getPriceEntitlement(price, allEnts);
|
||||
|
||||
return {
|
||||
description: li.description || "",
|
||||
@@ -89,7 +88,7 @@ export const previewToCheckoutRes = async ({
|
||||
fullCus: attachParams.customer,
|
||||
});
|
||||
|
||||
let curProduct = curCusProduct
|
||||
const curProduct = curCusProduct
|
||||
? await getProductResponse({
|
||||
product: cusProductToProduct({ cusProduct: curCusProduct }),
|
||||
features,
|
||||
@@ -100,7 +99,12 @@ export const previewToCheckoutRes = async ({
|
||||
|
||||
const total = lines.reduce((acc, line) => acc + line.amount, 0);
|
||||
|
||||
let nextCycle = undefined;
|
||||
let nextCycle:
|
||||
| {
|
||||
starts_at: number;
|
||||
total: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
notNullish(preview.due_next_cycle) &&
|
||||
@@ -108,7 +112,7 @@ export const previewToCheckoutRes = async ({
|
||||
) {
|
||||
let total = newProduct.items
|
||||
.reduce((acc, item) => {
|
||||
if (item.usage_model == UsageModel.PayPerUse) {
|
||||
if (item.usage_model === UsageModel.PayPerUse) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
@@ -116,8 +120,8 @@ export const previewToCheckoutRes = async ({
|
||||
return acc.plus(item.price || 0);
|
||||
}
|
||||
|
||||
let prepaidQuantity =
|
||||
attachParams.optionsList.find((o) => o.feature_id == item.feature_id)
|
||||
const prepaidQuantity =
|
||||
attachParams.optionsList.find((o) => o.feature_id === item.feature_id)
|
||||
?.quantity || 0;
|
||||
|
||||
return acc.plus(prepaidQuantity * (item.price || 0));
|
||||
@@ -126,8 +130,7 @@ export const previewToCheckoutRes = async ({
|
||||
|
||||
try {
|
||||
if (
|
||||
preview.due_next_cycle &&
|
||||
preview.due_next_cycle.line_items &&
|
||||
preview.due_next_cycle?.line_items &&
|
||||
preview.due_next_cycle.line_items.length > 0
|
||||
) {
|
||||
total = preview.due_next_cycle.line_items
|
||||
@@ -148,6 +151,28 @@ export const previewToCheckoutRes = async ({
|
||||
};
|
||||
}
|
||||
|
||||
// Options
|
||||
const options = attachParams.optionsList
|
||||
.map((o) => {
|
||||
const price = allPrices.find((p) => {
|
||||
if (isUsagePrice({ price: p })) {
|
||||
return (
|
||||
p.config.internal_feature_id === o.internal_feature_id ||
|
||||
p.config.feature_id === o.feature_id
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!price) return undefined;
|
||||
|
||||
return {
|
||||
feature_id: o.feature_id,
|
||||
quantity: o.quantity * (price.config.billing_units || 1),
|
||||
};
|
||||
})
|
||||
.filter(notNullish);
|
||||
|
||||
return CheckoutResponseSchema.parse({
|
||||
customer_id: attachParams.customer.id,
|
||||
lines,
|
||||
@@ -159,5 +184,6 @@ export const previewToCheckoutRes = async ({
|
||||
? preview.due_next_cycle.due_at
|
||||
: null,
|
||||
next_cycle: nextCycle,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import {
|
||||
AttachBranch,
|
||||
AttachConfig,
|
||||
type AttachConfig,
|
||||
BillingInterval,
|
||||
FullProduct,
|
||||
FreeTrial,
|
||||
type FullProduct,
|
||||
isTrialing,
|
||||
} from "@autumn/shared";
|
||||
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
|
||||
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
|
||||
import { getAlignedUnix } from "@/internal/products/prices/billingIntervalUtils2.js";
|
||||
import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
|
||||
import type { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import {
|
||||
attachParamsToProduct,
|
||||
getCustomerSub,
|
||||
} from "../attachUtils/convertAttachParams.js";
|
||||
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
|
||||
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import { isTrialing } from "@autumn/shared";
|
||||
import { getAlignedUnix } from "@/internal/products/prices/billingIntervalUtils2.js";
|
||||
import { formatUnixToDate } from "@/utils/genUtils.js";
|
||||
|
||||
// Just for new product...?
|
||||
const getNextCycleItems = async ({
|
||||
newProduct,
|
||||
attachParams,
|
||||
@@ -40,7 +39,8 @@ const getNextCycleItems = async ({
|
||||
trialEnds?: number | null;
|
||||
}) => {
|
||||
// 2. If free trial
|
||||
let nextCycleAt = undefined;
|
||||
let nextCycleAt: number | undefined;
|
||||
|
||||
if (attachParams.freeTrial) {
|
||||
if (trialEnds) {
|
||||
nextCycleAt = trialEnds;
|
||||
@@ -51,7 +51,7 @@ const getNextCycleItems = async ({
|
||||
now: attachParams.now,
|
||||
})! * 1000;
|
||||
}
|
||||
} else if (branch != AttachBranch.OneOff && anchor) {
|
||||
} else if (branch !== AttachBranch.OneOff && anchor) {
|
||||
// Yearly one
|
||||
const largestInterval = getLargestInterval({ prices: newProduct.prices });
|
||||
if (largestInterval) {
|
||||
@@ -69,7 +69,7 @@ const getNextCycleItems = async ({
|
||||
now: attachParams.now,
|
||||
logger,
|
||||
withPrepaid,
|
||||
anchor,
|
||||
// anchor,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -98,14 +98,14 @@ export const getNewProductPreview = async ({
|
||||
attachParams,
|
||||
});
|
||||
|
||||
let trialEnds = undefined;
|
||||
let trialEnds: number | undefined;
|
||||
|
||||
// Scenario where we update a current sub with new product (so no create sub)
|
||||
let anchor = undefined;
|
||||
let anchor: number | undefined;
|
||||
if (mergeSub && !config.disableMerge) {
|
||||
if (mergeCusProduct?.free_trial) {
|
||||
if (isTrialing({ cusProduct: mergeCusProduct, now: attachParams.now })) {
|
||||
trialEnds = mergeCusProduct.trial_ends_at;
|
||||
trialEnds = mergeCusProduct.trial_ends_at || undefined;
|
||||
attachParams.freeTrial = mergeCusProduct.free_trial;
|
||||
} else {
|
||||
attachParams.freeTrial = null;
|
||||
@@ -141,7 +141,7 @@ export const getNewProductPreview = async ({
|
||||
trialEnds,
|
||||
});
|
||||
|
||||
let options = getOptions({
|
||||
const options = getOptions({
|
||||
prodItems: mapToProductItems({
|
||||
prices: newProduct.prices,
|
||||
entitlements: newProduct.entitlements,
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import {
|
||||
AttachBranch,
|
||||
type AttachConfig,
|
||||
cusProductToPrices,
|
||||
cusProductToProduct,
|
||||
type FreeTrial,
|
||||
type FullCusProduct,
|
||||
isTrialing,
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
type PreviewLineItem,
|
||||
type Price,
|
||||
UsageModel,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type Stripe from "stripe";
|
||||
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
|
||||
import { getItemsForCurProduct } from "@/internal/invoices/previewItemUtils/getItemsForCurProduct.js";
|
||||
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { getAlignedUnix } from "@/internal/products/prices/billingIntervalUtils2.js";
|
||||
import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
|
||||
import { nullish } from "@/utils/genUtils.js";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import type { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import {
|
||||
attachParamsToProduct,
|
||||
attachParamToCusProducts,
|
||||
paramsToCurSub,
|
||||
} from "../attachUtils/convertAttachParams.js";
|
||||
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
|
||||
import { getItemsForCurProduct } from "@/internal/invoices/previewItemUtils/getItemsForCurProduct.js";
|
||||
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
|
||||
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
|
||||
import Stripe from "stripe";
|
||||
import {
|
||||
AttachBranch,
|
||||
FreeTrial,
|
||||
FullCusProduct,
|
||||
PreviewLineItem,
|
||||
Price,
|
||||
UsageModel,
|
||||
AttachConfig,
|
||||
UsagePriceConfig,
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { formatUnixToDate, nullish } from "@/utils/genUtils.js";
|
||||
import { isTrialing } from "@autumn/shared";
|
||||
import { cusProductToPrices } from "@autumn/shared";
|
||||
import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import {
|
||||
addIntervalToAnchor,
|
||||
getAlignedUnix,
|
||||
} from "@/internal/products/prices/billingIntervalUtils2.js";
|
||||
|
||||
const getNextCycleAt = ({
|
||||
prices,
|
||||
sub,
|
||||
@@ -46,7 +42,7 @@ const getNextCycleAt = ({
|
||||
curCusProduct,
|
||||
}: {
|
||||
prices: Price[];
|
||||
sub: Stripe.Subscription;
|
||||
sub?: Stripe.Subscription;
|
||||
now?: number;
|
||||
freeTrial?: FreeTrial | null;
|
||||
branch: AttachBranch;
|
||||
@@ -55,7 +51,7 @@ const getNextCycleAt = ({
|
||||
now = now || Date.now();
|
||||
|
||||
if (
|
||||
branch == AttachBranch.NewVersion &&
|
||||
branch === AttachBranch.NewVersion &&
|
||||
curCusProduct &&
|
||||
isTrialing({ cusProduct: curCusProduct, now })
|
||||
) {
|
||||
@@ -72,7 +68,7 @@ const getNextCycleAt = ({
|
||||
}
|
||||
|
||||
const largestInterval = getLargestInterval({ prices });
|
||||
if (nullish(largestInterval) || !sub.billing_cycle_anchor) return now;
|
||||
if (nullish(largestInterval) || !sub?.billing_cycle_anchor) return now;
|
||||
|
||||
const nextCycleAt = getAlignedUnix({
|
||||
anchor: sub.billing_cycle_anchor * 1000,
|
||||
@@ -101,17 +97,17 @@ const filterNoProratePrepaidItems = ({
|
||||
for (const option of attachParams.optionsList) {
|
||||
const { feature_id, internal_feature_id, quantity } = option;
|
||||
const prevQuantity = curSameProduct?.options.find(
|
||||
(o) => o.feature_id == feature_id,
|
||||
(o) => o.feature_id === feature_id,
|
||||
)?.quantity;
|
||||
|
||||
const curPrice = curPrices.find(
|
||||
(p) =>
|
||||
(p.config as UsagePriceConfig)?.internal_feature_id ==
|
||||
(p.config as UsagePriceConfig)?.internal_feature_id ===
|
||||
internal_feature_id && isPrepaidPrice({ price: p }),
|
||||
);
|
||||
|
||||
const onDecrease = curPrice?.proration_config?.on_decrease;
|
||||
const decreaseIsNone = onDecrease == OnDecrease.None;
|
||||
const decreaseIsNone = onDecrease === OnDecrease.None;
|
||||
|
||||
if (decreaseIsNone && prevQuantity && quantity < prevQuantity) {
|
||||
console.log(
|
||||
@@ -122,7 +118,7 @@ const filterNoProratePrepaidItems = ({
|
||||
|
||||
const onIncrease = curPrice?.proration_config?.on_increase;
|
||||
if (
|
||||
onIncrease == OnIncrease.ProrateNextCycle &&
|
||||
onIncrease === OnIncrease.ProrateNextCycle &&
|
||||
prevQuantity &&
|
||||
quantity > prevQuantity
|
||||
) {
|
||||
@@ -173,7 +169,7 @@ export const getUpgradeProductPreview = async ({
|
||||
|
||||
if (config?.disableTrial) attachParams.freeTrial = null;
|
||||
let freeTrial = attachParams.freeTrial;
|
||||
let anchor = sub ? sub.billing_cycle_anchor * 1000 : undefined;
|
||||
const anchor = sub ? sub.billing_cycle_anchor * 1000 : undefined;
|
||||
|
||||
if (
|
||||
config?.carryTrial &&
|
||||
@@ -194,7 +190,9 @@ export const getUpgradeProductPreview = async ({
|
||||
anchor,
|
||||
});
|
||||
|
||||
let dueNextCycle = undefined;
|
||||
let dueNextCycle:
|
||||
| { line_items: PreviewLineItem[]; due_at: number }
|
||||
| undefined;
|
||||
if (!isFreeProduct(newProduct.prices)) {
|
||||
const nextCycleAt = getNextCycleAt({
|
||||
prices: newProduct.prices,
|
||||
@@ -205,31 +203,33 @@ export const getUpgradeProductPreview = async ({
|
||||
curCusProduct,
|
||||
});
|
||||
|
||||
let nextCycleItems = await getItemsForNewProduct({
|
||||
const nextCycleItems = await getItemsForNewProduct({
|
||||
newProduct,
|
||||
attachParams,
|
||||
logger,
|
||||
withPrepaid,
|
||||
});
|
||||
|
||||
dueNextCycle = {
|
||||
line_items: nextCycleItems,
|
||||
due_at: nextCycleAt,
|
||||
};
|
||||
if (nextCycleAt) {
|
||||
dueNextCycle = {
|
||||
line_items: nextCycleItems,
|
||||
due_at: nextCycleAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let items = [...curPreviewItems, ...newPreviewItems];
|
||||
|
||||
for (const item of structuredClone(curPreviewItems)) {
|
||||
let priceId = item.price_id;
|
||||
let newItem = newPreviewItems.find((i) => i.price_id == priceId);
|
||||
const priceId = item.price_id;
|
||||
const newItem = newPreviewItems.find((i) => i.price_id === priceId);
|
||||
|
||||
if (!newItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let newItemAmount = new Decimal(newItem?.amount ?? 0).toDecimalPlaces(2);
|
||||
let curItemAmount = new Decimal(item.amount ?? 0).toDecimalPlaces(2);
|
||||
const newItemAmount = new Decimal(newItem?.amount ?? 0).toDecimalPlaces(2);
|
||||
const curItemAmount = new Decimal(item.amount ?? 0).toDecimalPlaces(2);
|
||||
|
||||
if (newItemAmount.add(curItemAmount).eq(0)) {
|
||||
items = items.filter((i) => i.price_id !== priceId);
|
||||
@@ -241,7 +241,7 @@ export const getUpgradeProductPreview = async ({
|
||||
.toDecimalPlaces(2)
|
||||
.toNumber();
|
||||
|
||||
let options = getOptions({
|
||||
const options = getOptions({
|
||||
prodItems: mapToProductItems({
|
||||
prices: newProduct.prices,
|
||||
entitlements: newProduct.entitlements,
|
||||
@@ -256,10 +256,10 @@ export const getUpgradeProductPreview = async ({
|
||||
|
||||
items = items.filter((item) => item.amount !== 0);
|
||||
|
||||
if (branch == AttachBranch.UpdatePrepaidQuantity) {
|
||||
items = items.filter((item) => item.usage_model == UsageModel.Prepaid);
|
||||
if (branch === AttachBranch.UpdatePrepaidQuantity) {
|
||||
items = items.filter((item) => item.usage_model === UsageModel.Prepaid);
|
||||
dueNextCycle!.line_items = dueNextCycle!.line_items.filter(
|
||||
(item) => item.usage_model == UsageModel.Prepaid,
|
||||
(item) => item.usage_model === UsageModel.Prepaid,
|
||||
);
|
||||
|
||||
items = filterNoProratePrepaidItems({
|
||||
@@ -279,11 +279,20 @@ export const getUpgradeProductPreview = async ({
|
||||
total: dueTodayAmt,
|
||||
};
|
||||
|
||||
if (branch == AttachBranch.SameCustomEnts) {
|
||||
if (branch === AttachBranch.SameCustomEnts) {
|
||||
dueToday = undefined;
|
||||
}
|
||||
|
||||
if (branch == AttachBranch.NewVersion && dueToday) {
|
||||
if (branch === AttachBranch.NewVersion && dueToday) {
|
||||
const curProduct = cusProductToProduct({ cusProduct: curCusProduct });
|
||||
const newProduct = attachParamsToProduct({ attachParams });
|
||||
|
||||
if (isFreeProduct(curProduct.prices) && !isFreeProduct(newProduct.prices)) {
|
||||
throw new Error(
|
||||
`Version ${curProduct.version} is free, cannot upgrade to version ${newProduct.version} which has a paid price`,
|
||||
);
|
||||
}
|
||||
|
||||
dueToday.line_items = [];
|
||||
dueToday.total = 0;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import {
|
||||
AppEnv,
|
||||
type AppEnv,
|
||||
CouponDurationType,
|
||||
CusExpand,
|
||||
FullCustomer,
|
||||
Organization,
|
||||
type FullCustomer,
|
||||
type Organization,
|
||||
RewardType,
|
||||
Subscription,
|
||||
} from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import type Stripe from "stripe";
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
|
||||
export const getCusRewards = async ({
|
||||
org,
|
||||
@@ -33,14 +31,14 @@ export const getCusRewards = async ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let stripeCli = createStripeCli({
|
||||
const stripeCli = createStripeCli({
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
const [stripeCus, stripeSubs] = await Promise.all([
|
||||
stripeCli.customers.retrieve(
|
||||
fullCus.processor?.id!,
|
||||
fullCus.processor?.id,
|
||||
) as Promise<Stripe.Customer>,
|
||||
getStripeSubs({
|
||||
stripeCli,
|
||||
@@ -49,7 +47,7 @@ export const getCusRewards = async ({
|
||||
}),
|
||||
]);
|
||||
|
||||
let stripeDiscounts: Stripe.Discount[] = stripeSubs?.flatMap(
|
||||
const stripeDiscounts: Stripe.Discount[] = stripeSubs?.flatMap(
|
||||
(s) => s.discounts,
|
||||
) as Stripe.Discount[];
|
||||
|
||||
@@ -57,7 +55,7 @@ export const getCusRewards = async ({
|
||||
stripeDiscounts.push(stripeCus.discount);
|
||||
}
|
||||
|
||||
let rewards = {
|
||||
const rewards = {
|
||||
discounts: stripeDiscounts.map((d) => {
|
||||
let duration_type: CouponDurationType;
|
||||
let duration_value = 0;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
type AppEnv,
|
||||
CusExpand,
|
||||
type FullCustomer,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
||||
import { lineItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { stripeDiscountToResponse } from "./stripeDiscountToResponse.js";
|
||||
|
||||
export const getCusUpcomingInvoice = async ({
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
fullCus,
|
||||
expand,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
fullCus: FullCustomer;
|
||||
expand: CusExpand[];
|
||||
}) => {
|
||||
if (!expand.includes(CusExpand.UpcomingInvoice)) return undefined;
|
||||
|
||||
const subIds = fullCus.customer_products.flatMap(
|
||||
(cp) => cp.subscription_ids || [],
|
||||
);
|
||||
|
||||
if (subIds.length === 0) return null;
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
|
||||
const subs = await getStripeSubs({
|
||||
stripeCli,
|
||||
subIds,
|
||||
});
|
||||
|
||||
const sub = subs.reduce((acc, sub) => {
|
||||
const curSubPeriodEnd = getEarliestPeriodEnd({ sub });
|
||||
const nextSubPeriodEnd = getEarliestPeriodEnd({ sub });
|
||||
return nextSubPeriodEnd < curSubPeriodEnd ? sub : acc;
|
||||
}, subs[0]);
|
||||
|
||||
const upcomingInvoice = await stripeCli.invoices.createPreview({
|
||||
customer: fullCus.processor?.id,
|
||||
subscription: sub.id,
|
||||
expand: ["discounts.coupon"],
|
||||
});
|
||||
|
||||
const lines = [];
|
||||
for (const line of upcomingInvoice.lines.data) {
|
||||
const cusProd = fullCus.customer_products.find((cp) =>
|
||||
lineItemInCusProduct({ cusProduct: cp, lineItem: line }),
|
||||
);
|
||||
lines.push({
|
||||
product_id: cusProd?.product.id || null,
|
||||
description: line.description,
|
||||
amount: new Decimal(line.amount).div(100).toDecimalPlaces(2).toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
const stripeDiscounts = upcomingInvoice.discounts.filter(
|
||||
(d): d is Stripe.Discount =>
|
||||
typeof d === "object" && d !== null && "coupon" in d,
|
||||
) as Stripe.Discount[];
|
||||
|
||||
// Get reward in IDs
|
||||
|
||||
const discounts = stripeDiscounts.map((d) =>
|
||||
stripeDiscountToResponse({
|
||||
discount: d,
|
||||
totalDiscountAmounts: upcomingInvoice.total_discount_amounts || undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// console.log("lines: ", lines);
|
||||
// console.log("discounts: ", discounts);
|
||||
|
||||
const res = {
|
||||
lines,
|
||||
discounts,
|
||||
subtotal: upcomingInvoice.subtotal / 100,
|
||||
total: upcomingInvoice.total / 100,
|
||||
currency: upcomingInvoice.currency,
|
||||
};
|
||||
|
||||
return res;
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { CouponDurationType, RewardType } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
|
||||
const parseStripeCouponDuration = (coupon: Stripe.Coupon) => {
|
||||
let duration_type: CouponDurationType = CouponDurationType.OneOff;
|
||||
let duration_value: number = 0;
|
||||
|
||||
if (coupon.duration === "forever") {
|
||||
duration_type = CouponDurationType.Forever;
|
||||
} else if (coupon.duration === "once") {
|
||||
duration_type = CouponDurationType.OneOff;
|
||||
} else if (coupon.duration === "repeating") {
|
||||
duration_type = CouponDurationType.Months;
|
||||
duration_value = coupon.duration_in_months || 0;
|
||||
} else {
|
||||
duration_type = CouponDurationType.OneOff;
|
||||
}
|
||||
|
||||
return {
|
||||
duration_type,
|
||||
duration_value,
|
||||
};
|
||||
};
|
||||
|
||||
export const stripeDiscountToResponse = ({
|
||||
discount,
|
||||
totalDiscountAmounts,
|
||||
}: {
|
||||
discount: Stripe.Discount;
|
||||
totalDiscountAmounts?: Stripe.Invoice.TotalDiscountAmount[];
|
||||
}) => {
|
||||
const d = discount;
|
||||
|
||||
const { duration_type, duration_value } = parseStripeCouponDuration(d.coupon);
|
||||
|
||||
const totalDiscountAmount = totalDiscountAmounts?.find(
|
||||
(t) => t.discount === d.id,
|
||||
);
|
||||
|
||||
return {
|
||||
id: d.coupon?.id,
|
||||
name: d.coupon?.name ?? "",
|
||||
type: d.coupon?.amount_off
|
||||
? RewardType.FixedDiscount
|
||||
: RewardType.PercentageDiscount,
|
||||
discount_value: d.coupon?.amount_off || d.coupon?.percent_off || 0,
|
||||
currency: d.coupon?.currency ?? null,
|
||||
start: d.start ?? null,
|
||||
end: d.end ?? null,
|
||||
// subscription_id: d.subscription ?? null,
|
||||
duration_type,
|
||||
duration_value,
|
||||
|
||||
total_discount_amount: totalDiscountAmount?.amount
|
||||
? totalDiscountAmount.amount / 100
|
||||
: null,
|
||||
};
|
||||
};
|
||||
@@ -1,33 +1,33 @@
|
||||
import { BREAK_API_VERSION } from "@/utils/constants.js";
|
||||
import {
|
||||
AppEnv,
|
||||
CusProductStatus,
|
||||
FullCusProduct,
|
||||
APIVersion,
|
||||
type AppEnv,
|
||||
CusEntResponseSchema,
|
||||
CusExpand,
|
||||
CusProductStatus,
|
||||
CusResponseSchema,
|
||||
CustomerResponseSchema,
|
||||
CusEntResponseSchema,
|
||||
FeatureType,
|
||||
Feature,
|
||||
Organization,
|
||||
FullCustomer,
|
||||
CusExpand,
|
||||
RewardResponse,
|
||||
cusProductsToCusEnts,
|
||||
cusProductsToCusPrices,
|
||||
EntityResponseSchema,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
type Organization,
|
||||
type RewardResponse,
|
||||
} from "@autumn/shared";
|
||||
import { getCusInvoices } from "./cusUtils.js";
|
||||
|
||||
import { orgToVersion } from "@/utils/versionUtils.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { cusProductsToCusEnts, cusProductsToCusPrices } from "@autumn/shared";
|
||||
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { invoicesToResponse } from "@/internal/invoices/invoiceUtils.js";
|
||||
import { getCusBalances } from "./cusFeatureResponseUtils/getCusBalances.js";
|
||||
import { BREAK_API_VERSION } from "@/utils/constants.js";
|
||||
import { orgToVersion } from "@/utils/versionUtils.js";
|
||||
import { featuresToObject } from "./cusFeatureResponseUtils/balancesToFeatureResponse.js";
|
||||
import { getCusBalances } from "./cusFeatureResponseUtils/getCusBalances.js";
|
||||
import { processFullCusProducts } from "./cusProductResponseUtils/processFullCusProducts.js";
|
||||
import { getCusPaymentMethodRes } from "./cusResponseUtils/getCusPaymentMethodRes.js";
|
||||
import { getCusReferrals } from "./cusResponseUtils/getCusReferrals.js";
|
||||
import { getCusRewards } from "./cusResponseUtils/getCusRewards.js";
|
||||
import { getCusPaymentMethodRes } from "./cusResponseUtils/getCusPaymentMethodRes.js";
|
||||
import { getCusUpcomingInvoice } from "./cusResponseUtils/getCusUpcomingInvoice.js";
|
||||
import { getCusInvoices } from "./cusUtils.js";
|
||||
|
||||
export const getCustomerDetails = async ({
|
||||
db,
|
||||
@@ -52,18 +52,18 @@ export const getCustomerDetails = async ({
|
||||
expand: CusExpand[];
|
||||
reqApiVersion?: number;
|
||||
}) => {
|
||||
let apiVersion = orgToVersion({
|
||||
const apiVersion = orgToVersion({
|
||||
org,
|
||||
reqApiVersion,
|
||||
});
|
||||
|
||||
let withRewards = expand.includes(CusExpand.Rewards);
|
||||
const withRewards = expand.includes(CusExpand.Rewards);
|
||||
|
||||
let inStatuses = org.config.include_past_due
|
||||
const inStatuses = org.config.include_past_due
|
||||
? [CusProductStatus.Active, CusProductStatus.PastDue]
|
||||
: [CusProductStatus.Active];
|
||||
|
||||
let cusEnts = cusProductsToCusEnts({ cusProducts, inStatuses }) as any;
|
||||
const cusEnts = cusProductsToCusEnts({ cusProducts, inStatuses }) as any;
|
||||
|
||||
const balances = await getCusBalances({
|
||||
cusEntsWithCusProduct: cusEnts,
|
||||
@@ -72,7 +72,7 @@ export const getCustomerDetails = async ({
|
||||
apiVersion,
|
||||
});
|
||||
|
||||
let subIds = cusProducts.flatMap(
|
||||
const subIds = cusProducts.flatMap(
|
||||
(cp: FullCusProduct) => cp.subscription_ids || [],
|
||||
);
|
||||
|
||||
@@ -87,8 +87,8 @@ export const getCustomerDetails = async ({
|
||||
|
||||
if (apiVersion >= APIVersion.v1_1) {
|
||||
let entList: any = balances.map((b) => {
|
||||
let isBoolean =
|
||||
features.find((f: Feature) => f.id == b.feature_id)?.type ==
|
||||
const isBoolean =
|
||||
features.find((f: Feature) => f.id === b.feature_id)?.type ===
|
||||
FeatureType.Boolean;
|
||||
if (b.unlimited || isBoolean) {
|
||||
return b;
|
||||
@@ -101,7 +101,7 @@ export const getCustomerDetails = async ({
|
||||
});
|
||||
});
|
||||
|
||||
let products: any = [...main, ...addOns];
|
||||
const products: any = [...main, ...addOns];
|
||||
|
||||
if (apiVersion >= APIVersion.v1_2) {
|
||||
entList = featuresToObject({
|
||||
@@ -110,9 +110,9 @@ export const getCustomerDetails = async ({
|
||||
});
|
||||
}
|
||||
|
||||
let withInvoices = expand.includes(CusExpand.Invoices);
|
||||
const withInvoices = expand.includes(CusExpand.Invoices);
|
||||
|
||||
let rewards: RewardResponse | undefined = await getCusRewards({
|
||||
const rewards: RewardResponse | undefined = await getCusRewards({
|
||||
org,
|
||||
env,
|
||||
fullCus: customer,
|
||||
@@ -120,26 +120,35 @@ export const getCustomerDetails = async ({
|
||||
expand,
|
||||
});
|
||||
|
||||
let referrals = await getCusReferrals({
|
||||
const upcomingInvoice = await getCusUpcomingInvoice({
|
||||
db,
|
||||
fullCus: customer,
|
||||
expand,
|
||||
});
|
||||
|
||||
let paymentMethod = await getCusPaymentMethodRes({
|
||||
org,
|
||||
env,
|
||||
fullCus: customer,
|
||||
expand,
|
||||
});
|
||||
|
||||
let cusResponse = {
|
||||
const referrals = await getCusReferrals({
|
||||
db,
|
||||
fullCus: customer,
|
||||
expand,
|
||||
});
|
||||
|
||||
const paymentMethod = await getCusPaymentMethodRes({
|
||||
org,
|
||||
env,
|
||||
fullCus: customer,
|
||||
expand,
|
||||
});
|
||||
|
||||
const cusResponse = {
|
||||
...CusResponseSchema.parse({
|
||||
...customer,
|
||||
stripe_id: customer.processor?.id,
|
||||
features: entList,
|
||||
products,
|
||||
// invoices: withInvoices ? invoices : undefined,
|
||||
|
||||
invoices: withInvoices
|
||||
? invoicesToResponse({
|
||||
invoices: customer.invoices || [],
|
||||
@@ -165,6 +174,7 @@ export const getCustomerDetails = async ({
|
||||
: undefined,
|
||||
referrals,
|
||||
payment_method: paymentMethod,
|
||||
upcoming_invoice: upcomingInvoice,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -177,7 +187,7 @@ export const getCustomerDetails = async ({
|
||||
return cusResponse;
|
||||
}
|
||||
} else {
|
||||
let withItems = org.config.api_version >= BREAK_API_VERSION;
|
||||
const withItems = org.config.api_version >= BREAK_API_VERSION;
|
||||
|
||||
const processedInvoices = await getCusInvoices({
|
||||
db,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { APIVersion, CusExpand, ErrCode } from "@autumn/shared";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js";
|
||||
import { parseCusExpand } from "../cusUtils/cusUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { orgToVersion } from "@/utils/versionUtils.js";
|
||||
import { getCusWithCache } from "../cusCache/getCusWithCache.js";
|
||||
import { parseCusExpand } from "../cusUtils/cusUtils.js";
|
||||
import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js";
|
||||
|
||||
export const handleGetCustomer = async (req: any, res: any) =>
|
||||
routeHandler({
|
||||
@@ -12,18 +12,18 @@ export const handleGetCustomer = async (req: any, res: any) =>
|
||||
res,
|
||||
action: "get customer",
|
||||
handler: async () => {
|
||||
let customerId = req.params.customer_id;
|
||||
let { env, db, logtail: logger, org, features } = req;
|
||||
let { expand } = req.query;
|
||||
const customerId = req.params.customer_id;
|
||||
const { env, db, logtail: logger, org, features } = req;
|
||||
const { expand } = req.query;
|
||||
|
||||
let expandArray = parseCusExpand(expand);
|
||||
const expandArray = parseCusExpand(expand);
|
||||
|
||||
let apiVersion = orgToVersion({
|
||||
const apiVersion = orgToVersion({
|
||||
org,
|
||||
reqApiVersion: req.apiVersion,
|
||||
});
|
||||
|
||||
let getInvoices = apiVersion < APIVersion.v1_1;
|
||||
const getInvoices = apiVersion < APIVersion.v1_1;
|
||||
if (getInvoices) expandArray.push(CusExpand.Invoices);
|
||||
|
||||
logger.info(`getting customer ${customerId} for org ${org.slug}`);
|
||||
@@ -51,7 +51,7 @@ export const handleGetCustomer = async (req: any, res: any) =>
|
||||
return;
|
||||
}
|
||||
|
||||
let cusData = await getCustomerDetails({
|
||||
const cusData = await getCustomerDetails({
|
||||
db,
|
||||
customer,
|
||||
org,
|
||||
|
||||
@@ -1,54 +1,51 @@
|
||||
import {
|
||||
EntitlementWithFeature,
|
||||
FullProduct,
|
||||
Organization,
|
||||
Price,
|
||||
Feature,
|
||||
BillingInterval,
|
||||
FreeTrial,
|
||||
PreviewLineItem,
|
||||
BillingType,
|
||||
type EntitlementWithFeature,
|
||||
type Feature,
|
||||
type FreeTrial,
|
||||
type FullProduct,
|
||||
getFeatureInvoiceDescription,
|
||||
UsagePriceConfig,
|
||||
UsageModel,
|
||||
AttachConfig,
|
||||
AttachBranch,
|
||||
ProrationBehavior,
|
||||
IntervalConfig,
|
||||
} from "@autumn/shared";
|
||||
import { AttachParams } from "../../customers/cusProducts/AttachParams.js";
|
||||
import {
|
||||
formatPrice,
|
||||
getBillingType,
|
||||
getPriceForOverage,
|
||||
getPriceOptions,
|
||||
} from "../../products/prices/priceUtils.js";
|
||||
import { getPriceEntitlement } from "../../products/prices/priceUtils.js";
|
||||
import {
|
||||
isFixedPrice,
|
||||
isOneOffPrice,
|
||||
isPrepaidPrice,
|
||||
type IntervalConfig,
|
||||
isUsagePrice,
|
||||
} from "../../products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
|
||||
import { newPriceToInvoiceDescription } from "../invoiceFormatUtils.js";
|
||||
import { calculateProrationAmount } from "../prorationUtils.js";
|
||||
import { getPricecnPrice } from "../../products/pricecn/pricecnUtils.js";
|
||||
import { toProductItem } from "@autumn/shared";
|
||||
import { formatAmount } from "@/utils/formatUtils.js";
|
||||
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
|
||||
import { subtractIntervalForProration } from "../../products/prices/billingIntervalUtils.js";
|
||||
type Organization,
|
||||
type PreviewLineItem,
|
||||
type Price,
|
||||
toProductItem,
|
||||
UsageModel,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type Stripe from "stripe";
|
||||
import { attachParamsToCurCusProduct } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
|
||||
import { getContUseInvoiceItems } from "@/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.js";
|
||||
import { getAlignedUnix } from "@/internal/products/prices/billingIntervalUtils2.js";
|
||||
import {
|
||||
priceToFeature,
|
||||
priceToUsageModel,
|
||||
} from "@/internal/products/prices/priceUtils/convertPrice.js";
|
||||
import { getContUseInvoiceItems } from "@/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.js";
|
||||
import Stripe from "stripe";
|
||||
import { attachParamsToCurCusProduct } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
|
||||
import { sortPricesByType } from "@/internal/products/prices/priceUtils/sortPriceUtils.js";
|
||||
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { getAlignedUnix } from "@/internal/products/prices/billingIntervalUtils2.js";
|
||||
import { sortPricesByType } from "@/internal/products/prices/priceUtils/sortPriceUtils.js";
|
||||
import { formatAmount } from "@/utils/formatUtils.js";
|
||||
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
|
||||
import type { AttachParams } from "../../customers/cusProducts/AttachParams.js";
|
||||
import { getPricecnPrice } from "../../products/pricecn/pricecnUtils.js";
|
||||
import { subtractIntervalForProration } from "../../products/prices/billingIntervalUtils.js";
|
||||
import {
|
||||
isFixedPrice,
|
||||
isOneOffPrice,
|
||||
isPrepaidPrice,
|
||||
} from "../../products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
|
||||
import {
|
||||
formatPrice,
|
||||
getBillingType,
|
||||
getPriceEntitlement,
|
||||
getPriceForOverage,
|
||||
getPriceOptions,
|
||||
} from "../../products/prices/priceUtils.js";
|
||||
import { newPriceToInvoiceDescription } from "../invoiceFormatUtils.js";
|
||||
import { calculateProrationAmount } from "../prorationUtils.js";
|
||||
|
||||
export const getDefaultPriceStr = ({
|
||||
org,
|
||||
@@ -94,7 +91,7 @@ export const getProration = ({
|
||||
intervalCount = intervalCount ?? 1;
|
||||
now = now || Date.now();
|
||||
|
||||
if (interval == BillingInterval.OneOff) return undefined;
|
||||
if (interval === BillingInterval.OneOff) return undefined;
|
||||
|
||||
let end = proration?.end;
|
||||
if (!end && anchor) {
|
||||
@@ -220,7 +217,7 @@ export const getItemsForNewProduct = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
if (billingType == BillingType.UsageInArrear) {
|
||||
if (billingType === BillingType.UsageInArrear) {
|
||||
items.push({
|
||||
price: getDefaultPriceStr({ org, price, ent: ent!, features }),
|
||||
description: newPriceToInvoiceDescription({
|
||||
@@ -236,8 +233,8 @@ export const getItemsForNewProduct = async ({
|
||||
}
|
||||
|
||||
if (withPrepaid && isPrepaidPrice({ price })) {
|
||||
let options = getPriceOptions(price, attachParams.optionsList);
|
||||
let quantity = notNullish(options?.quantity) ? options?.quantity! : 1;
|
||||
const options = getPriceOptions(price, attachParams.optionsList);
|
||||
const quantity = notNullish(options?.quantity) ? options?.quantity! : 1;
|
||||
|
||||
const quantityWithBillingUnits = new Decimal(quantity).mul(
|
||||
(price.config as UsagePriceConfig).billing_units || 1,
|
||||
@@ -245,14 +242,14 @@ export const getItemsForNewProduct = async ({
|
||||
|
||||
// console.log("price", price);
|
||||
// console.log("Quantity", quantity);
|
||||
let amount = priceToInvoiceAmount({
|
||||
const amount = priceToInvoiceAmount({
|
||||
price,
|
||||
quantity: quantityWithBillingUnits.toNumber(),
|
||||
proration: finalProration,
|
||||
now,
|
||||
});
|
||||
// console.log("Amount", amount);
|
||||
let feature = priceToFeature({
|
||||
const feature = priceToFeature({
|
||||
price,
|
||||
features,
|
||||
})!;
|
||||
@@ -281,7 +278,7 @@ export const getItemsForNewProduct = async ({
|
||||
attachParams,
|
||||
});
|
||||
|
||||
let { newItems } = await getContUseInvoiceItems({
|
||||
const { newItems } = await getContUseInvoiceItems({
|
||||
cusProduct,
|
||||
sub,
|
||||
attachParams,
|
||||
|
||||
@@ -1,163 +1,158 @@
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
BillingType,
|
||||
CusProductStatus,
|
||||
ErrCode,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import express, { Router } from "express";
|
||||
import { constructMigrationJob } from "@/internal/migrations/migrationUtils.js";
|
||||
import { BillingType, ErrCode, type UsagePriceConfig } from "@autumn/shared";
|
||||
import express, { type Router } from "express";
|
||||
import { MigrationService } from "@/internal/migrations/MigrationService.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import { constructMigrationJob } from "@/internal/migrations/migrationUtils.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import {
|
||||
getBillingType,
|
||||
pricesOnlyOneOff,
|
||||
} from "@/internal/products/prices/priceUtils.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import type {
|
||||
ExtendedRequest,
|
||||
ExtendedResponse,
|
||||
} from "@/utils/models/Request.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { findPrepaidPrice } from "../products/prices/priceUtils/findPriceUtils.js";
|
||||
|
||||
export const migrationRouter: Router = express.Router();
|
||||
|
||||
export const handleMigrate = async (
|
||||
req: ExtendedRequest,
|
||||
res?: ExtendedResponse,
|
||||
) => {
|
||||
const { orgId, env, db, features } = req;
|
||||
|
||||
const { from_product_id, from_version, to_product_id, to_version } = req.body;
|
||||
|
||||
const fromProduct = await ProductService.getFull({
|
||||
db,
|
||||
env,
|
||||
orgId,
|
||||
idOrInternalId: from_product_id,
|
||||
version: from_version,
|
||||
});
|
||||
|
||||
const toProduct = await ProductService.getFull({
|
||||
db,
|
||||
env,
|
||||
orgId,
|
||||
idOrInternalId: to_product_id,
|
||||
version: to_version,
|
||||
});
|
||||
|
||||
if (isFreeProduct(fromProduct.prices) && !isFreeProduct(toProduct.prices)) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers from free product to paid product`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if from product is one off, or to product is one off
|
||||
if (
|
||||
pricesOnlyOneOff(fromProduct.prices) ||
|
||||
pricesOnlyOneOff(toProduct.prices)
|
||||
) {
|
||||
const fromIsOneOff = pricesOnlyOneOff(fromProduct.prices);
|
||||
const msg = fromIsOneOff
|
||||
? `${fromProduct.name} is a one off product, cannot migrate customers on it`
|
||||
: `${toProduct.name} is a one off product, cannot migrate customers to this product`;
|
||||
|
||||
throw new RecaseError({
|
||||
message: msg,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (fromProduct.is_add_on || toProduct.is_add_on) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers for add on products`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
for (const price of toProduct.prices) {
|
||||
const billingType = getBillingType(price.config);
|
||||
if (billingType !== BillingType.UsageInAdvance) continue;
|
||||
|
||||
const config = price.config as UsagePriceConfig;
|
||||
const internalFeatureId = config.internal_feature_id;
|
||||
const feature = features.find((f) => f.internal_id === internalFeatureId);
|
||||
|
||||
const prepaidPrice = findPrepaidPrice({
|
||||
prices: fromProduct.prices,
|
||||
internalFeatureId,
|
||||
});
|
||||
|
||||
if (!prepaidPrice) {
|
||||
throw new RecaseError({
|
||||
message: `New product has prepaid price for feature ${feature?.name}, but old product does not, can't perform migration`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFreeProduct(fromProduct.prices) && isFreeProduct(toProduct.prices)) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers from paid product to free product`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (isFreeProduct(fromProduct.prices) && !isFreeProduct(toProduct.prices)) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers from free product to paid product`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Create migration JOB
|
||||
const migrationJob = constructMigrationJob({
|
||||
fromProduct,
|
||||
toProduct,
|
||||
});
|
||||
|
||||
await MigrationService.createJob({
|
||||
db,
|
||||
data: migrationJob,
|
||||
});
|
||||
|
||||
if (!fromProduct || !toProduct) {
|
||||
throw new RecaseError({
|
||||
message: `Product ${from_product_id} version ${from_version} or ${to_product_id} version ${to_version} not found`,
|
||||
code: ErrCode.ProductNotFound,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.Migration,
|
||||
payload: {
|
||||
migrationJobId: migrationJob.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res) {
|
||||
res.status(200).json(migrationJob);
|
||||
}
|
||||
};
|
||||
|
||||
migrationRouter.post("", async (req: any, res: any) => {
|
||||
return routeHandler({
|
||||
req,
|
||||
res,
|
||||
action: "migrate",
|
||||
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
|
||||
const { orgId, env, db, features } = req;
|
||||
|
||||
const { from_product_id, from_version, to_product_id, to_version } =
|
||||
req.body;
|
||||
|
||||
let fromProduct = await ProductService.getFull({
|
||||
db,
|
||||
env,
|
||||
orgId,
|
||||
idOrInternalId: from_product_id,
|
||||
version: from_version,
|
||||
});
|
||||
|
||||
let toProduct = await ProductService.getFull({
|
||||
db,
|
||||
env,
|
||||
orgId,
|
||||
idOrInternalId: to_product_id,
|
||||
version: to_version,
|
||||
});
|
||||
|
||||
if (
|
||||
isFreeProduct(fromProduct.prices) &&
|
||||
!isFreeProduct(toProduct.prices)
|
||||
) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers from free product to paid product`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if from product is one off, or to product is one off
|
||||
if (
|
||||
pricesOnlyOneOff(fromProduct.prices) ||
|
||||
pricesOnlyOneOff(toProduct.prices)
|
||||
) {
|
||||
let fromIsOneOff = pricesOnlyOneOff(fromProduct.prices);
|
||||
let msg = fromIsOneOff
|
||||
? `${fromProduct.name} is a one off product, cannot migrate customers on it`
|
||||
: `${toProduct.name} is a one off product, cannot migrate customers to this product`;
|
||||
|
||||
throw new RecaseError({
|
||||
message: msg,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (fromProduct.is_add_on || toProduct.is_add_on) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers for add on products`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
for (const price of toProduct.prices) {
|
||||
let billingType = getBillingType(price.config);
|
||||
if (billingType != BillingType.UsageInAdvance) continue;
|
||||
|
||||
let config = price.config as UsagePriceConfig;
|
||||
let internalFeatureId = config.internal_feature_id;
|
||||
let feature = features.find((f) => f.internal_id == internalFeatureId)!;
|
||||
|
||||
for (const price of fromProduct.prices) {
|
||||
let prepaidPrice = findPrepaidPrice({
|
||||
prices: fromProduct.prices,
|
||||
internalFeatureId,
|
||||
});
|
||||
|
||||
if (!prepaidPrice) {
|
||||
throw new RecaseError({
|
||||
message: `New product has prepaid price for feature ${feature.name}, but old product does not, can't perform migration`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!isFreeProduct(fromProduct.prices) &&
|
||||
isFreeProduct(toProduct.prices)
|
||||
) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers from paid product to free product`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isFreeProduct(fromProduct.prices) &&
|
||||
!isFreeProduct(toProduct.prices)
|
||||
) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers from free product to paid product`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Create migration JOB
|
||||
let migrationJob = constructMigrationJob({
|
||||
fromProduct,
|
||||
toProduct,
|
||||
});
|
||||
|
||||
await MigrationService.createJob({
|
||||
db,
|
||||
data: migrationJob,
|
||||
});
|
||||
|
||||
if (!fromProduct || !toProduct) {
|
||||
throw new RecaseError({
|
||||
message: `Product ${from_product_id} version ${from_version} or ${to_product_id} version ${to_version} not found`,
|
||||
code: ErrCode.ProductNotFound,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.Migration,
|
||||
payload: {
|
||||
migrationJobId: migrationJob.id,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json(migrationJob);
|
||||
await handleMigrate(req, res);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import {
|
||||
type AppEnv,
|
||||
type DiscountConfig,
|
||||
type FixedPriceConfig,
|
||||
type FullProduct,
|
||||
type Price,
|
||||
type UsagePriceConfig,
|
||||
DiscountConfig,
|
||||
PriceType,
|
||||
RewardType,
|
||||
getBillingType,
|
||||
isFixedPrice,
|
||||
isUsagePrice,
|
||||
type Price,
|
||||
PriceType,
|
||||
RewardType,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import type { logger as loggerType } from "@/external/logtail/logtailUtils.js";
|
||||
import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js";
|
||||
import type { JobName } from "@/queue/JobName.js";
|
||||
import type { Payloads } from "@/queue/queueUtils.js";
|
||||
import { RewardService } from "../rewards/RewardService.js";
|
||||
import { tiersAreSame } from "../products/prices/priceInitUtils.js";
|
||||
import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js";
|
||||
import { PriceService } from "../products/prices/PriceService.js";
|
||||
import { OrgService } from "../orgs/OrgService.js";
|
||||
import { formatPrice } from "../products/prices/priceUtils.js";
|
||||
import { ProductService } from "../products/ProductService.js";
|
||||
import { PriceService } from "../products/prices/PriceService.js";
|
||||
import { tiersAreSame } from "../products/prices/priceInitUtils.js";
|
||||
import { RewardService } from "../rewards/RewardService.js";
|
||||
|
||||
// Helper function to check if tier structures match
|
||||
const tiersMatch = (oldTiers: any[], newTiers: any[]): boolean => {
|
||||
@@ -65,8 +63,8 @@ const findMatchingUsagePrice = (
|
||||
return false;
|
||||
|
||||
// Match by billing behavior
|
||||
let newBillingType = getBillingType(newConfig);
|
||||
let oldBillingType = getBillingType(oldConfig);
|
||||
const newBillingType = getBillingType(newConfig);
|
||||
const oldBillingType = getBillingType(oldConfig);
|
||||
if (newBillingType !== oldBillingType) return false;
|
||||
|
||||
// Optionally match by tier structure
|
||||
@@ -93,7 +91,7 @@ const findBestMatch = (oldPrice: Price, newPrices: Price[]): Price | null => {
|
||||
getBillingType(newPrice.config) === getBillingType(oldPrice.config) &&
|
||||
newPrice.config.interval === oldPrice.config.interval &&
|
||||
newPrice.config.interval_count === oldPrice.config.interval_count &&
|
||||
(oldConfig.type == PriceType.Usage
|
||||
(oldConfig.type === PriceType.Usage
|
||||
? oldConfig.internal_feature_id === newConfig.internal_feature_id
|
||||
: true)
|
||||
);
|
||||
|
||||
@@ -2,73 +2,50 @@ import {
|
||||
APIVersion,
|
||||
BillingInterval,
|
||||
BillingType,
|
||||
FullCusProduct,
|
||||
type FullCusProduct,
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
Price,
|
||||
UsagePriceConfig,
|
||||
type Price,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { formatPrice, getBillingType } from "../../priceUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import Stripe from "stripe";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type Stripe from "stripe";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { getBillingType } from "../../priceUtils.js";
|
||||
|
||||
export const isOneOffPrice = ({ price }: { price: Price }) => {
|
||||
return price.config.interval == BillingInterval.OneOff;
|
||||
};
|
||||
|
||||
export const isUsagePrice = ({
|
||||
price,
|
||||
featureId,
|
||||
}: {
|
||||
price: Price;
|
||||
featureId?: string;
|
||||
}) => {
|
||||
let billingType = getBillingType(price.config);
|
||||
|
||||
let isUsage =
|
||||
billingType == BillingType.UsageInArrear ||
|
||||
billingType == BillingType.InArrearProrated ||
|
||||
billingType == BillingType.UsageInAdvance;
|
||||
|
||||
if (featureId) {
|
||||
return (
|
||||
isUsage && (price.config as UsagePriceConfig).feature_id == featureId
|
||||
);
|
||||
}
|
||||
|
||||
return isUsage;
|
||||
return price.config.interval === BillingInterval.OneOff;
|
||||
};
|
||||
|
||||
export const isArrearPrice = ({ price }: { price?: Price }) => {
|
||||
if (!price) return false;
|
||||
let billingType = getBillingType(price.config);
|
||||
return billingType == BillingType.UsageInArrear;
|
||||
const billingType = getBillingType(price.config);
|
||||
return billingType === BillingType.UsageInArrear;
|
||||
};
|
||||
export const isContUsePrice = ({ price }: { price?: Price }) => {
|
||||
if (!price) return false;
|
||||
let billingType = getBillingType(price.config);
|
||||
return billingType == BillingType.InArrearProrated;
|
||||
const billingType = getBillingType(price.config);
|
||||
return billingType === BillingType.InArrearProrated;
|
||||
};
|
||||
|
||||
export const isPrepaidPrice = ({ price }: { price: Price }) => {
|
||||
let billingType = getBillingType(price.config);
|
||||
return billingType == BillingType.UsageInAdvance;
|
||||
const billingType = getBillingType(price.config);
|
||||
return billingType === BillingType.UsageInAdvance;
|
||||
};
|
||||
|
||||
export const isPayPerUse = ({ price }: { price: Price }) => {
|
||||
let billingType = getBillingType(price.config);
|
||||
const billingType = getBillingType(price.config);
|
||||
return (
|
||||
billingType == BillingType.UsageInArrear ||
|
||||
billingType == BillingType.InArrearProrated
|
||||
billingType === BillingType.UsageInArrear ||
|
||||
billingType === BillingType.InArrearProrated
|
||||
);
|
||||
};
|
||||
|
||||
export const isFixedPrice = ({ price }: { price: Price }) => {
|
||||
let billingType = getBillingType(price.config);
|
||||
const billingType = getBillingType(price.config);
|
||||
|
||||
return (
|
||||
billingType == BillingType.FixedCycle || billingType == BillingType.OneOff
|
||||
billingType === BillingType.FixedCycle || billingType === BillingType.OneOff
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,8 +57,8 @@ export const hasPrepaidPrice = ({
|
||||
excludeOneOff?: boolean;
|
||||
}) => {
|
||||
return prices.some((price) => {
|
||||
let isUsage = getBillingType(price.config) == BillingType.UsageInAdvance;
|
||||
let isOneOff = price.config.interval == BillingInterval.OneOff;
|
||||
const isUsage = getBillingType(price.config) === BillingType.UsageInAdvance;
|
||||
const isOneOff = price.config.interval === BillingInterval.OneOff;
|
||||
|
||||
return isUsage && (excludeOneOff ? !isOneOff : true);
|
||||
});
|
||||
@@ -97,8 +74,8 @@ export const isV4Usage = ({
|
||||
const billingType = getBillingType(price.config);
|
||||
|
||||
return (
|
||||
billingType == BillingType.UsageInArrear &&
|
||||
(cusProduct.api_version == APIVersion.v1_4 ||
|
||||
billingType === BillingType.UsageInArrear &&
|
||||
(cusProduct.api_version === APIVersion.v1_4 ||
|
||||
notNullish(cusProduct.internal_entity_id))
|
||||
);
|
||||
};
|
||||
@@ -143,10 +120,10 @@ export const roundUsage = ({
|
||||
price: Price;
|
||||
pos?: boolean;
|
||||
}) => {
|
||||
let config = price.config as UsagePriceConfig;
|
||||
let billingUnits = config.billing_units || 1;
|
||||
const config = price.config as UsagePriceConfig;
|
||||
const billingUnits = config.billing_units || 1;
|
||||
|
||||
let rounded = new Decimal(usage)
|
||||
const rounded = new Decimal(usage)
|
||||
.div(billingUnits)
|
||||
.ceil()
|
||||
.mul(billingUnits)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullProduct, Product, ProductV2 } from "@autumn/shared";
|
||||
import type { FullProduct } from "@autumn/shared";
|
||||
import { isProductUpgrade } from "../productUtils.js";
|
||||
|
||||
export const sortProductsByPrice = ({
|
||||
@@ -7,7 +7,7 @@ export const sortProductsByPrice = ({
|
||||
products: FullProduct[];
|
||||
}) => {
|
||||
products.sort((a, b) => {
|
||||
let isUpgradeA = isProductUpgrade({
|
||||
const isUpgradeA = isProductUpgrade({
|
||||
prices1: a.prices,
|
||||
prices2: b.prices,
|
||||
usageAlwaysUpgrade: false,
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
type AppEnv,
|
||||
ErrCode,
|
||||
type Reward,
|
||||
type RewardType,
|
||||
rewards,
|
||||
RewardType,
|
||||
} from "@autumn/shared";
|
||||
import { and, desc, eq, inArray, or, sql } from "drizzle-orm";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
@@ -39,6 +39,26 @@ export class RewardService {
|
||||
return result as Reward;
|
||||
}
|
||||
|
||||
static async getInIds({
|
||||
db,
|
||||
ids,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
ids: string[];
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) {
|
||||
return (await db.query.rewards.findMany({
|
||||
where: and(
|
||||
inArray(rewards.id, ids),
|
||||
eq(rewards.org_id, orgId),
|
||||
eq(rewards.env, env),
|
||||
),
|
||||
})) as Reward[];
|
||||
}
|
||||
|
||||
static async getByIdOrCode({
|
||||
db,
|
||||
codes,
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import Stripe from "stripe";
|
||||
import {
|
||||
BillingInterval,
|
||||
type CusProductStatus,
|
||||
type EntitlementWithFeature,
|
||||
type FullCustomer,
|
||||
type FullProduct,
|
||||
isUsagePrice,
|
||||
type Price,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
||||
import { stripeToAutumnSubStatus } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { subToAutumnInterval } from "@/external/stripe/utils.js";
|
||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { PriceService } from "@/internal/products/prices/PriceService.js";
|
||||
import { SubService } from "@/internal/subscriptions/SubService.js";
|
||||
import { constructSub } from "@/internal/subscriptions/subUtils.js";
|
||||
import {
|
||||
FullCustomer,
|
||||
FullProduct,
|
||||
Price,
|
||||
EntitlementWithFeature,
|
||||
CusProductStatus,
|
||||
UsagePriceConfig,
|
||||
BillingInterval,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { notNullish } from "../genUtils.js";
|
||||
import { ExtendedRequest } from "../models/Request.js";
|
||||
import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { subToAutumnInterval } from "@/external/stripe/utils.js";
|
||||
import { prices as priceTable } from "@autumn/shared";
|
||||
import { PriceService } from "@/internal/products/prices/PriceService.js";
|
||||
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
||||
import type { ExtendedRequest } from "../models/Request.js";
|
||||
|
||||
export const addProductFromSubs = async ({
|
||||
req,
|
||||
@@ -48,17 +45,17 @@ export const addProductFromSubs = async ({
|
||||
const cusProducts = autumnCus.customer_products;
|
||||
const entity = autumnCus.entity;
|
||||
|
||||
let mainCusProduct = cusProducts.find(
|
||||
const mainCusProduct = cusProducts.find(
|
||||
(cp) =>
|
||||
!cp.product.is_add_on &&
|
||||
cp.product_id == autumnProduct.id &&
|
||||
cp.product_id === autumnProduct.id &&
|
||||
(notNullish(entity)
|
||||
? cp.internal_entity_id == entity!.internal_id
|
||||
? cp.internal_entity_id === entity!.internal_id
|
||||
: true),
|
||||
);
|
||||
|
||||
if (mainCusProduct && !force) {
|
||||
let prices = mainCusProduct.customer_prices.map((cp) => cp.price);
|
||||
const prices = mainCusProduct.customer_prices.map((cp) => cp.price);
|
||||
// let isFree = isFreeProduct(prices);
|
||||
|
||||
if (mainCusProduct) {
|
||||
@@ -74,12 +71,12 @@ export const addProductFromSubs = async ({
|
||||
}
|
||||
|
||||
// Handle if trialing
|
||||
let trialEndsAt = sub?.trial_end ? sub.trial_end * 1000 : null;
|
||||
const trialEndsAt = sub?.trial_end ? sub.trial_end * 1000 : null;
|
||||
|
||||
// throw new Error("test");
|
||||
|
||||
// 1. Insert custom prices...
|
||||
let customPrices = prices?.filter((p) => p.is_custom);
|
||||
const customPrices = prices?.filter((p) => p.is_custom);
|
||||
if (customPrices && customPrices.length > 0) {
|
||||
await PriceService.upsert({
|
||||
db,
|
||||
@@ -91,7 +88,7 @@ export const addProductFromSubs = async ({
|
||||
sub,
|
||||
});
|
||||
|
||||
let newCusProduct = await createFullCusProduct({
|
||||
const newCusProduct = await createFullCusProduct({
|
||||
db,
|
||||
attachParams: {
|
||||
replaceables: [],
|
||||
@@ -132,16 +129,16 @@ export const addProductFromSubs = async ({
|
||||
|
||||
if (sub) {
|
||||
// Create sub
|
||||
let usageFeatures = autumnProduct.prices
|
||||
const usageFeatures = autumnProduct.prices
|
||||
.filter((p) => isUsagePrice({ price: p }))
|
||||
.map((p) => (p.config as UsagePriceConfig).internal_feature_id);
|
||||
|
||||
let subFromDb = await SubService.getInStripeIds({
|
||||
const subFromDb = await SubService.getInStripeIds({
|
||||
db,
|
||||
ids: [sub.id],
|
||||
});
|
||||
|
||||
let subInterval = subToAutumnInterval(sub);
|
||||
const subInterval = subToAutumnInterval(sub);
|
||||
|
||||
if (subFromDb.length === 0) {
|
||||
await SubService.createSub({
|
||||
@@ -149,7 +146,7 @@ export const addProductFromSubs = async ({
|
||||
sub: constructSub({
|
||||
stripeId: sub.id,
|
||||
usageFeatures:
|
||||
subInterval.interval == BillingInterval.Month ? usageFeatures : [],
|
||||
subInterval.interval === BillingInterval.Month ? usageFeatures : [],
|
||||
orgId: org.id,
|
||||
env,
|
||||
currentPeriodStart: start,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { type AppEnv, type CreateReward, isUsagePrice } from "@autumn/shared";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import type { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { AppEnv, CreateReward, Product, ProductV2 } from "@autumn/shared";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
|
||||
export const createProduct = async ({
|
||||
db,
|
||||
@@ -43,7 +42,7 @@ export const createProduct = async ({
|
||||
await Promise.all(batchDelete);
|
||||
} catch (error) {}
|
||||
|
||||
let clone = structuredClone(product);
|
||||
const clone = structuredClone(product);
|
||||
if (typeof clone.items === "object") {
|
||||
clone.items = Object.values(clone.items);
|
||||
}
|
||||
@@ -106,14 +105,14 @@ export const createReward = async ({
|
||||
productId: string;
|
||||
onlyUsage?: boolean;
|
||||
}) => {
|
||||
let fullProduct = await ProductService.getFull({
|
||||
const fullProduct = await ProductService.getFull({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
idOrInternalId: productId!,
|
||||
});
|
||||
|
||||
let usagePrices = fullProduct?.prices.filter((price) =>
|
||||
const usagePrices = fullProduct?.prices.filter((price) =>
|
||||
isUsagePrice({ price }),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { AttachScenario } from "../checkModels/checkPreviewModels.js";
|
||||
import { FeatureOptionsSchema } from "../cusProductModels/cusProductModels.js";
|
||||
import { ProductItemResponseSchema } from "../productV2Models/productItemModels/prodItemResponseModels.js";
|
||||
import { ProductResponseSchema } from "../productV2Models/productResponseModels.js";
|
||||
import { FeatureOptionsSchema } from "../cusProductModels/cusProductModels.js";
|
||||
|
||||
export const CheckoutLineSchema = z.object({
|
||||
description: z.string(),
|
||||
|
||||
@@ -5,4 +5,5 @@ export enum CusExpand {
|
||||
Entities = "entities",
|
||||
Referrals = "referrals",
|
||||
PaymentMethod = "payment_method",
|
||||
UpcomingInvoice = "upcoming_invoice",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { z } from "zod";
|
||||
import { DiscountResponseSchema } from "../../rewardModels/rewardModels/rewardResponseModels.js";
|
||||
|
||||
export const UpcomingInvoiceResponseSchema = z.object({
|
||||
lines: z.array(
|
||||
z.object({
|
||||
product_id: z.string().nullish(),
|
||||
description: z.string(),
|
||||
amount: z.number(),
|
||||
}),
|
||||
),
|
||||
discounts: z.array(DiscountResponseSchema),
|
||||
subtotal: z.number(),
|
||||
total: z.number(),
|
||||
currency: z.string(),
|
||||
});
|
||||
|
||||
export type UpcomingInvoiceResponse = z.infer<
|
||||
typeof UpcomingInvoiceResponseSchema
|
||||
>;
|
||||
@@ -3,6 +3,7 @@ import { AppEnv } from "../genModels/genEnums.js";
|
||||
import { RewardResponseSchema } from "../rewardModels/rewardModels/rewardResponseModels.js";
|
||||
import { CusProductResponseSchema } from "./cusResModels/cusProductResponse.js";
|
||||
import { CusReferralResponseSchema } from "./cusResModels/cusReferralsResponse.js";
|
||||
import { UpcomingInvoiceResponseSchema } from "./cusResModels/upcomingInvoiceResponse.js";
|
||||
import { EntityResponseSchema } from "./entityModels/entityResModels.js";
|
||||
import { InvoiceResponseSchema } from "./invoiceModels/invoiceResponseModels.js";
|
||||
|
||||
@@ -34,6 +35,7 @@ export const CusResponseSchema = z.object({
|
||||
entities: z.array(EntityResponseSchema).optional(),
|
||||
referrals: z.array(CusReferralResponseSchema).optional(),
|
||||
payment_method: z.any().nullish(),
|
||||
upcoming_invoice: UpcomingInvoiceResponseSchema.nullish(),
|
||||
});
|
||||
|
||||
export type CusResponse = z.infer<typeof CusResponseSchema>;
|
||||
|
||||
@@ -6,8 +6,14 @@ export const FixedPriceConfigSchema = z.object({
|
||||
amount: z.number().min(0),
|
||||
interval: z.nativeEnum(BillingInterval),
|
||||
interval_count: z.number().nullish(),
|
||||
|
||||
// Usage price fields
|
||||
billing_units: z.number().nullish(),
|
||||
stripe_price_id: z.string().nullish(),
|
||||
stripe_empty_price_id: z.string().nullish(),
|
||||
stripe_product_id: z.null().or(z.undefined()),
|
||||
feature_id: z.null().or(z.undefined()),
|
||||
internal_feature_id: z.null().or(z.undefined()),
|
||||
});
|
||||
|
||||
export type FixedPriceConfig = z.infer<typeof FixedPriceConfigSchema>;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { RewardType } from "./rewardEnums.js";
|
||||
import { CouponDurationType } from "./rewardEnums.js";
|
||||
import { CouponDurationType, RewardType } from "./rewardEnums.js";
|
||||
|
||||
export const DiscountResponseSchema = z.object({
|
||||
id: z.string(), // either from Autumn or Stripe
|
||||
@@ -17,7 +16,8 @@ export const DiscountResponseSchema = z.object({
|
||||
start: z.number().nullish(),
|
||||
end: z.number().nullish(),
|
||||
|
||||
subscription_id: z.string().nullable(),
|
||||
subscription_id: z.string().nullish(),
|
||||
total_discount_amount: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const RewardResponseSchema = z.object({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { Reward } from "../rewardModels/rewardModels.js";
|
||||
import { RewardReceivedBy } from "./rewardProgramEnums.js";
|
||||
import { RewardTriggerEvent } from "./rewardProgramEnums.js";
|
||||
import type { Reward } from "../rewardModels/rewardModels.js";
|
||||
import { RewardReceivedBy, RewardTriggerEvent } from "./rewardProgramEnums.js";
|
||||
|
||||
export const RewardProgram = z.object({
|
||||
internal_id: z.string(),
|
||||
@@ -33,6 +32,15 @@ export const CreateRewardProgram = z.object({
|
||||
received_by: z.nativeEnum(RewardReceivedBy),
|
||||
});
|
||||
|
||||
export const UpdateRewardProgram = z.object({
|
||||
when: z.nativeEnum(RewardTriggerEvent),
|
||||
product_ids: z.array(z.string()).optional(),
|
||||
exclude_trial: z.boolean().optional(),
|
||||
internal_reward_id: z.string(),
|
||||
max_redemptions: z.number().optional(),
|
||||
received_by: z.nativeEnum(RewardReceivedBy),
|
||||
});
|
||||
|
||||
export type RewardProgram = z.infer<typeof RewardProgram>;
|
||||
export type CreateRewardProgram = z.infer<typeof CreateRewardProgram>;
|
||||
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import Stripe from "stripe";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { notNullish } from "../utils.js";
|
||||
import { FixedPriceConfig } from "../../models/productModels/priceModels/priceConfig/fixedPriceConfig.js";
|
||||
import type Stripe from "stripe";
|
||||
import { APIVersion } from "../../enums/APIVersion.js";
|
||||
import type { FullCusProduct } from "../../models/cusProductModels/cusProductModels.js";
|
||||
import type { FixedPriceConfig } from "../../models/productModels/priceModels/priceConfig/fixedPriceConfig.js";
|
||||
import {
|
||||
BillWhen,
|
||||
UsagePriceConfig,
|
||||
type UsagePriceConfig,
|
||||
} from "../../models/productModels/priceModels/priceConfig/usagePriceConfig.js";
|
||||
import {
|
||||
BillingInterval,
|
||||
BillingType,
|
||||
PriceType,
|
||||
} from "../../models/productModels/priceModels/priceEnums.js";
|
||||
import { Price } from "../../models/productModels/priceModels/priceModels.js";
|
||||
import { FullCusProduct } from "../../models/cusProductModels/cusProductModels.js";
|
||||
import type { Price } from "../../models/productModels/priceModels/priceModels.js";
|
||||
import {
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
} from "../../models/productV2Models/productItemModels/productItemEnums.js";
|
||||
import { APIVersion } from "../../enums/APIVersion.js";
|
||||
import { notNullish } from "../utils.js";
|
||||
|
||||
export const getBillingType = (config: FixedPriceConfig | UsagePriceConfig) => {
|
||||
// 1. Fixed cycle / one off
|
||||
if (
|
||||
config.type == PriceType.Fixed &&
|
||||
config.interval == BillingInterval.OneOff
|
||||
config.type === PriceType.Fixed &&
|
||||
config.interval === BillingInterval.OneOff
|
||||
) {
|
||||
return BillingType.OneOff;
|
||||
} else if (config.type == PriceType.Fixed) {
|
||||
} else if (config.type === PriceType.Fixed) {
|
||||
return BillingType.FixedCycle;
|
||||
}
|
||||
|
||||
// 2. Prepaid
|
||||
|
||||
let usageConfig = config as UsagePriceConfig;
|
||||
const usageConfig = config as UsagePriceConfig;
|
||||
if (
|
||||
usageConfig.bill_when == BillWhen.InAdvance ||
|
||||
usageConfig.bill_when == BillWhen.StartOfPeriod
|
||||
usageConfig.bill_when === BillWhen.InAdvance ||
|
||||
usageConfig.bill_when === BillWhen.StartOfPeriod
|
||||
) {
|
||||
return BillingType.UsageInAdvance;
|
||||
} else if (usageConfig.bill_when == BillWhen.EndOfPeriod) {
|
||||
} else if (usageConfig.bill_when === BillWhen.EndOfPeriod) {
|
||||
if (usageConfig.should_prorate) {
|
||||
return BillingType.InArrearProrated;
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export const getBillingType = (config: FixedPriceConfig | UsagePriceConfig) => {
|
||||
};
|
||||
|
||||
export const isOneOffPrice = ({ price }: { price: Price }) => {
|
||||
return price.config.interval == BillingInterval.OneOff;
|
||||
return price.config.interval === BillingInterval.OneOff;
|
||||
};
|
||||
|
||||
export const isUsagePrice = ({
|
||||
@@ -59,40 +59,38 @@ export const isUsagePrice = ({
|
||||
price: Price;
|
||||
featureId?: string;
|
||||
}) => {
|
||||
let billingType = getBillingType(price.config);
|
||||
const billingType = getBillingType(price.config);
|
||||
|
||||
let isUsage =
|
||||
billingType == BillingType.UsageInArrear ||
|
||||
billingType == BillingType.InArrearProrated ||
|
||||
billingType == BillingType.UsageInAdvance;
|
||||
const isUsage =
|
||||
billingType === BillingType.UsageInArrear ||
|
||||
billingType === BillingType.InArrearProrated ||
|
||||
billingType === BillingType.UsageInAdvance;
|
||||
|
||||
if (featureId) {
|
||||
return (
|
||||
isUsage && (price.config as UsagePriceConfig).feature_id == featureId
|
||||
);
|
||||
return isUsage && price.config.feature_id === featureId;
|
||||
}
|
||||
|
||||
return isUsage;
|
||||
};
|
||||
|
||||
export const isPrepaidPrice = ({ price }: { price: Price }) => {
|
||||
let billingType = getBillingType(price.config);
|
||||
return billingType == BillingType.UsageInAdvance;
|
||||
const billingType = getBillingType(price.config);
|
||||
return billingType === BillingType.UsageInAdvance;
|
||||
};
|
||||
|
||||
export const isPayPerUse = ({ price }: { price: Price }) => {
|
||||
let billingType = getBillingType(price.config);
|
||||
const billingType = getBillingType(price.config);
|
||||
return (
|
||||
billingType == BillingType.UsageInArrear ||
|
||||
billingType == BillingType.InArrearProrated
|
||||
billingType === BillingType.UsageInArrear ||
|
||||
billingType === BillingType.InArrearProrated
|
||||
);
|
||||
};
|
||||
|
||||
export const isFixedPrice = ({ price }: { price: Price }) => {
|
||||
let billingType = getBillingType(price.config);
|
||||
const billingType = getBillingType(price.config);
|
||||
|
||||
return (
|
||||
billingType == BillingType.FixedCycle || billingType == BillingType.OneOff
|
||||
billingType === BillingType.FixedCycle || billingType === BillingType.OneOff
|
||||
);
|
||||
};
|
||||
|
||||
@@ -104,8 +102,8 @@ export const hasPrepaidPrice = ({
|
||||
excludeOneOff?: boolean;
|
||||
}) => {
|
||||
return prices.some((price) => {
|
||||
let isUsage = getBillingType(price.config) == BillingType.UsageInAdvance;
|
||||
let isOneOff = price.config.interval == BillingInterval.OneOff;
|
||||
const isUsage = getBillingType(price.config) === BillingType.UsageInAdvance;
|
||||
const isOneOff = price.config.interval === BillingInterval.OneOff;
|
||||
return isUsage && (excludeOneOff ? !isOneOff : true);
|
||||
});
|
||||
};
|
||||
@@ -120,8 +118,8 @@ export const isV4Usage = ({
|
||||
const billingType = getBillingType(price.config);
|
||||
|
||||
return (
|
||||
billingType == BillingType.UsageInArrear &&
|
||||
(cusProduct.api_version == APIVersion.v1_4 ||
|
||||
billingType === BillingType.UsageInArrear &&
|
||||
(cusProduct.api_version === APIVersion.v1_4 ||
|
||||
notNullish(cusProduct.internal_entity_id))
|
||||
);
|
||||
};
|
||||
@@ -166,10 +164,10 @@ export const roundUsage = ({
|
||||
price: Price;
|
||||
pos?: boolean;
|
||||
}) => {
|
||||
let config = price.config as UsagePriceConfig;
|
||||
let billingUnits = config.billing_units || 1;
|
||||
const config = price.config as UsagePriceConfig;
|
||||
const billingUnits = config.billing_units || 1;
|
||||
|
||||
let rounded = new Decimal(usage)
|
||||
const rounded = new Decimal(usage)
|
||||
.div(billingUnits)
|
||||
.ceil()
|
||||
.mul(billingUnits)
|
||||
|
||||
@@ -4,9 +4,13 @@ import type {
|
||||
Reward,
|
||||
RewardType,
|
||||
UsagePriceConfig,
|
||||
UsageTier,
|
||||
} from "../../index.js";
|
||||
import type { UsageTier } from "../../models/productModels/priceModels/priceConfig/usagePriceConfig.js";
|
||||
import { isFixedPrice, isUsagePrice } from "../productUtils/priceUtils.js";
|
||||
import {
|
||||
getBillingType,
|
||||
isFixedPrice,
|
||||
isUsagePrice,
|
||||
} from "../productUtils/priceUtils.js";
|
||||
|
||||
// Helper function to check if tier structures match
|
||||
const tiersMatch = (oldTiers: UsageTier[], newTiers: UsageTier[]): boolean => {
|
||||
@@ -42,6 +46,8 @@ const findMatchingUsagePrice = (
|
||||
|
||||
return (
|
||||
candidates.find((candidate) => {
|
||||
if (!isUsagePrice({ price: candidate })) return false;
|
||||
|
||||
const newConfig = candidate.config as UsagePriceConfig;
|
||||
|
||||
// Match by feature
|
||||
@@ -50,8 +56,9 @@ const findMatchingUsagePrice = (
|
||||
return false;
|
||||
|
||||
// Match by billing behavior
|
||||
if (newConfig.bill_when !== oldConfig.bill_when) return false;
|
||||
if (newConfig.should_prorate !== oldConfig.should_prorate) return false;
|
||||
const newBilingType = getBillingType(newConfig);
|
||||
const oldBilingType = getBillingType(oldConfig);
|
||||
if (newBilingType !== oldBilingType) return false;
|
||||
|
||||
// Optionally match by tier structure
|
||||
if (!tiersMatch(oldConfig.usage_tiers, newConfig.usage_tiers))
|
||||
|
||||
@@ -51,6 +51,7 @@ export const RewardProgramConfig = ({
|
||||
<div className="w-6/12">
|
||||
<FieldLabel>Program ID</FieldLabel>
|
||||
<Input
|
||||
disabled={isUpdate}
|
||||
value={rewardProgram.id || ""}
|
||||
onChange={(e) =>
|
||||
setRewardProgram({ ...rewardProgram, id: e.target.value })
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// import { useProductsContext } from "../ProductsContext";
|
||||
|
||||
import { type RewardProgram, RewardTriggerEvent } from "@autumn/shared";
|
||||
import { useState } from "react";
|
||||
import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { RewardProgram, RewardTriggerEvent } from "@autumn/shared";
|
||||
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { AdminHover } from "@/components/general/AdminHover";
|
||||
// import { RewardProgramRowToolbar } from "./RewardProgramRowToolbar";
|
||||
import { Item, Row } from "@/components/general/TableGrid";
|
||||
import { AdminHover } from "@/components/general/AdminHover";
|
||||
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
|
||||
import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { RewardProgramRowToolbar } from "./RewardProgramRowToolbar";
|
||||
import UpdateRewardProgram from "./UpdateRewardPrograms";
|
||||
|
||||
@@ -58,7 +59,11 @@ export const RewardProgramsTable = () => {
|
||||
</AdminHover>
|
||||
</Item>
|
||||
<Item className="col-span-4">
|
||||
<span className="truncate">{rewardProgram.when}</span>
|
||||
<span className="truncate">
|
||||
{rewardProgram.when === RewardTriggerEvent.CustomerCreation
|
||||
? "Customer Redemption"
|
||||
: keyToTitle(rewardProgram.when)}
|
||||
</span>
|
||||
</Item>
|
||||
<Item className="col-span-4">
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -70,9 +75,9 @@ export const RewardProgramsTable = () => {
|
||||
</div>
|
||||
</Item>
|
||||
<Item className="col-span-3">
|
||||
{rewardProgram.when == RewardTriggerEvent.CustomerCreation
|
||||
{rewardProgram.when === RewardTriggerEvent.CustomerCreation
|
||||
? "Sign Up"
|
||||
: rewardProgram.when == RewardTriggerEvent.Checkout
|
||||
: rewardProgram.when === RewardTriggerEvent.Checkout
|
||||
? "Checkout"
|
||||
: keyToTitle(rewardProgram.when)}
|
||||
</Item>
|
||||
|
||||
Reference in New Issue
Block a user