fix: components/pricing_table exports item data in field
This commit is contained in:
@@ -23,7 +23,7 @@ elif [[ $filename == *"/tests/"* ]]; then
|
||||
elif [[ $filename == *".sh"* ]]; then
|
||||
$filename
|
||||
else
|
||||
npx tsx $filename
|
||||
NODE_ENV=development npx tsx $filename
|
||||
fi
|
||||
|
||||
|
||||
|
||||
@@ -2,34 +2,9 @@ import pino from "pino";
|
||||
|
||||
export const initLogger = () => {
|
||||
// Create separate streams for console and HyperDX
|
||||
const streams: pino.StreamEntry[] = [
|
||||
// Pretty console output stream
|
||||
// {
|
||||
// level: process.env.NODE_ENV === "development" ? "debug" : "info",
|
||||
// stream: pino.transport({
|
||||
// target: "pino-pretty",
|
||||
// options: {
|
||||
// colorize: true,
|
||||
// translateTime: "UTC:yyyy-mm-dd HH:MM:ss",
|
||||
// ignore: "pid,hostname,res,context,req,statusCode,worker",
|
||||
// customColors: {
|
||||
// default: "white",
|
||||
// 60: "bgRed",
|
||||
// 50: "red",
|
||||
// 40: "yellow",
|
||||
// 30: "green",
|
||||
// 20: "blue",
|
||||
// 10: "gray",
|
||||
// message: "reset",
|
||||
// greyMessage: "gray",
|
||||
// time: "darkGray",
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// },
|
||||
];
|
||||
const streams: pino.StreamEntry[] = [];
|
||||
|
||||
if (process.env.NODE_ENV == "development") {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
streams.push({
|
||||
level: process.env.NODE_ENV === "development" ? "debug" : "info",
|
||||
stream: pino.transport({
|
||||
|
||||
4
server/src/external/stripe/stripeWebhooks.ts
vendored
4
server/src/external/stripe/stripeWebhooks.ts
vendored
@@ -88,8 +88,10 @@ stripeWebhookRouter.post(
|
||||
request.logtail = request.logtail.child({
|
||||
context: {
|
||||
context: {
|
||||
body: request.body,
|
||||
// body: request.body,
|
||||
event_type: event.type,
|
||||
event_id: event.id,
|
||||
object_id: `${event.data?.object?.id}` || "N/A",
|
||||
authType: AuthType.Stripe,
|
||||
org_id: orgId,
|
||||
org_slug: org.slug,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { handleCheckoutSub } from "./handleCheckoutCompleted/handleCheckoutSub.js";
|
||||
import { handleRemainingSets } from "./handleCheckoutCompleted/handleRemainingSets.js";
|
||||
import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js";
|
||||
import { getEntityInvoiceDescription } from "@/internal/entities/entityUtils/entityInvoiceUtils.js";
|
||||
|
||||
export const handleCheckoutSessionCompleted = async ({
|
||||
req,
|
||||
@@ -130,6 +131,7 @@ export const handleCheckoutSessionCompleted = async ({
|
||||
|
||||
console.log("✅ checkout.completed: successfully created cus product");
|
||||
const batchInsertInvoice: any = [];
|
||||
|
||||
for (const invoiceId of invoiceIds) {
|
||||
batchInsertInvoice.push(
|
||||
insertInvoiceFromAttach({
|
||||
|
||||
@@ -329,8 +329,6 @@ export const handleInvoiceCreated = async ({
|
||||
let entity = await EntityService.getByInternalId({
|
||||
db,
|
||||
internalId: internalEntityId,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
let feature = features.find(
|
||||
@@ -352,7 +350,7 @@ export const handleInvoiceCreated = async ({
|
||||
feature,
|
||||
plural: false,
|
||||
capitalize: true,
|
||||
})}: ${entity?.name} (ID: ${entity?.id})`,
|
||||
})}: ${entDetails}`,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -45,21 +45,13 @@ export class EntityService {
|
||||
static async getByInternalId({
|
||||
db,
|
||||
internalId,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
internalId: string;
|
||||
orgId: string;
|
||||
env: string;
|
||||
}) {
|
||||
let entity = await db.query.entities.findFirst({
|
||||
where: (entities, { eq, and }) =>
|
||||
and(
|
||||
eq(entities.internal_id, internalId),
|
||||
eq(entities.org_id, orgId),
|
||||
eq(entities.env, env),
|
||||
),
|
||||
and(eq(entities.internal_id, internalId)),
|
||||
});
|
||||
if (!entity) {
|
||||
throw new RecaseError({
|
||||
|
||||
@@ -13,6 +13,8 @@ import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingInter
|
||||
import { APIVersion } from "@autumn/shared";
|
||||
import { SuccessCode } from "@autumn/shared";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { getEntityInvoiceDescription } from "@/internal/entities/entityUtils/entityInvoiceUtils.js";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const handleCreateCheckout = async ({
|
||||
req,
|
||||
@@ -67,13 +69,14 @@ export const handleCreateCheckout = async ({
|
||||
);
|
||||
}
|
||||
|
||||
const subscriptionData = isRecurring
|
||||
const subscriptionData:
|
||||
| Stripe.Checkout.SessionCreateParams.SubscriptionData
|
||||
| undefined = isRecurring
|
||||
? {
|
||||
trial_end:
|
||||
freeTrial && !attachParams.disableFreeTrial
|
||||
? freeTrialToStripeTimestamp({ freeTrial })
|
||||
: undefined,
|
||||
// metadata: subMeta,
|
||||
billing_cycle_anchor: billingCycleAnchorUnixSeconds,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -49,12 +49,14 @@ export const cusProductsToCusEnts = ({
|
||||
cusProducts,
|
||||
inStatuses = [CusProductStatus.Active],
|
||||
reverseOrder = false,
|
||||
featureId,
|
||||
}: {
|
||||
cusProducts: FullCusProduct[];
|
||||
inStatuses?: CusProductStatus[];
|
||||
reverseOrder?: boolean;
|
||||
featureId?: string;
|
||||
}) => {
|
||||
const cusEnts: FullCustomerEntitlement[] = [];
|
||||
let cusEnts: FullCustomerEntitlement[] = [];
|
||||
|
||||
for (const cusProduct of cusProducts) {
|
||||
if (!inStatuses.includes(cusProduct.status)) {
|
||||
@@ -69,6 +71,12 @@ export const cusProductsToCusEnts = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (featureId) {
|
||||
cusEnts = cusEnts.filter(
|
||||
(cusEnt) => cusEnt.entitlement.feature_id === featureId,
|
||||
);
|
||||
}
|
||||
|
||||
sortCusEntsForDeduction(cusEnts, reverseOrder);
|
||||
|
||||
return cusEnts;
|
||||
|
||||
@@ -29,25 +29,17 @@ import {
|
||||
|
||||
export const createNewCustomer = async ({
|
||||
req,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customer,
|
||||
nextResetAt,
|
||||
processor,
|
||||
logger,
|
||||
createDefaultProducts = true,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
db: DrizzleCli;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
customer: CreateCustomer;
|
||||
nextResetAt?: number;
|
||||
processor?: any;
|
||||
logger: any;
|
||||
createDefaultProducts?: boolean;
|
||||
}) => {
|
||||
const { db, org, env, logger } = req;
|
||||
|
||||
logger.info(
|
||||
`Creating customer: ${customer.email || customer.id}, org: ${org.slug}`,
|
||||
);
|
||||
@@ -65,17 +57,24 @@ export const createNewCustomer = async ({
|
||||
|
||||
const customerData: Customer = {
|
||||
...parsedCustomer,
|
||||
|
||||
name: parsedCustomer.name || "",
|
||||
email:
|
||||
nonFreeProds.length > 0 && !parsedCustomer.email
|
||||
? `${parsedCustomer.id}@invoices.useautumn.com`
|
||||
: parsedCustomer.email || "",
|
||||
|
||||
metadata: parsedCustomer.metadata || {},
|
||||
internal_id: generateId("cus"),
|
||||
org_id: org.id,
|
||||
created_at: Date.now(),
|
||||
env,
|
||||
processor,
|
||||
processor: parsedCustomer.stripe_id
|
||||
? {
|
||||
id: parsedCustomer.stripe_id,
|
||||
type: "stripe",
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Check if stripeCli exists
|
||||
|
||||
@@ -74,7 +74,6 @@ export const getOrCreateCustomer = async ({
|
||||
try {
|
||||
customer = await handleCreateCustomer({
|
||||
req,
|
||||
db,
|
||||
cusData: {
|
||||
id: customerId,
|
||||
name: customerData?.name,
|
||||
@@ -82,9 +81,6 @@ export const getOrCreateCustomer = async ({
|
||||
fingerprint: customerData?.fingerprint,
|
||||
metadata: customerData?.metadata || {},
|
||||
},
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
customer = await CusService.getFull({
|
||||
|
||||
@@ -59,23 +59,15 @@ export const initStripeCusAndProducts = async ({
|
||||
|
||||
const handleIdIsNull = async ({
|
||||
req,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
newCus,
|
||||
logger,
|
||||
processor,
|
||||
createDefaultProducts,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
db: DrizzleCli;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
newCus: CreateCustomer;
|
||||
logger: any;
|
||||
processor?: any;
|
||||
createDefaultProducts?: boolean;
|
||||
}) => {
|
||||
const { db, org, env, logger } = req;
|
||||
|
||||
// 1. ID is null
|
||||
if (!newCus.email) {
|
||||
throw new RecaseError({
|
||||
@@ -113,12 +105,7 @@ const handleIdIsNull = async ({
|
||||
|
||||
const createdCustomer = await createNewCustomer({
|
||||
req,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customer: newCus,
|
||||
logger,
|
||||
processor,
|
||||
createDefaultProducts,
|
||||
});
|
||||
|
||||
@@ -128,23 +115,15 @@ const handleIdIsNull = async ({
|
||||
// CAN ALSO USE DURING MIGRATION...
|
||||
export const handleCreateCustomerWithId = async ({
|
||||
req,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
newCus,
|
||||
processor,
|
||||
createDefaultProducts = true,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
db: DrizzleCli;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
logger: any;
|
||||
newCus: CreateCustomer;
|
||||
processor?: any;
|
||||
createDefaultProducts?: boolean;
|
||||
}) => {
|
||||
const { db, org, env, logger } = req;
|
||||
|
||||
// 1. Get by ID
|
||||
let existingCustomer = await CusService.get({
|
||||
db,
|
||||
@@ -155,7 +134,7 @@ export const handleCreateCustomerWithId = async ({
|
||||
|
||||
if (existingCustomer) {
|
||||
logger.info(
|
||||
`POST /customers, existing customer found: ${existingCustomer.id} (org: ${org.slug})`,
|
||||
`Customer already exists, skipping creation: ${existingCustomer.id}`,
|
||||
);
|
||||
return existingCustomer;
|
||||
}
|
||||
@@ -191,33 +170,18 @@ export const handleCreateCustomerWithId = async ({
|
||||
// 2. Handle email step...
|
||||
return await createNewCustomer({
|
||||
req,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customer: newCus,
|
||||
logger,
|
||||
processor,
|
||||
createDefaultProducts,
|
||||
});
|
||||
};
|
||||
|
||||
export const handleCreateCustomer = async ({
|
||||
req,
|
||||
db,
|
||||
cusData,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
processor,
|
||||
createDefaultProducts = true,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
db: DrizzleCli;
|
||||
cusData: CreateCustomer;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
logger: any;
|
||||
processor?: any;
|
||||
createDefaultProducts?: boolean;
|
||||
}) => {
|
||||
const newCus = CreateCustomerSchema.parse(cusData);
|
||||
@@ -227,23 +191,13 @@ export const handleCreateCustomer = async ({
|
||||
if (newCus.id === null) {
|
||||
createdCustomer = await handleIdIsNull({
|
||||
req,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
newCus,
|
||||
logger,
|
||||
processor,
|
||||
createDefaultProducts,
|
||||
});
|
||||
} else {
|
||||
createdCustomer = await handleCreateCustomerWithId({
|
||||
req,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
newCus,
|
||||
processor,
|
||||
createDefaultProducts,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { EntityService } from "@/internal/api/entities/EntityService.js";
|
||||
import { Feature, getFeatureName } from "@autumn/shared";
|
||||
import { AppEnv, Entity } from "autumn-js";
|
||||
|
||||
export const getEntityInvoiceDescription = async ({
|
||||
db,
|
||||
internalEntityId,
|
||||
features,
|
||||
logger,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
internalEntityId: string;
|
||||
features: Feature[];
|
||||
logger: any;
|
||||
}) => {
|
||||
try {
|
||||
let entity = await EntityService.getByInternalId({
|
||||
db,
|
||||
internalId: internalEntityId,
|
||||
});
|
||||
|
||||
let feature = features.find(
|
||||
(f) => f.internal_id == entity?.internal_feature_id,
|
||||
);
|
||||
|
||||
let entDetails = "";
|
||||
if (entity.name) {
|
||||
entDetails = `${entity.name}${entity.id ? ` (ID: ${entity.id})` : ""}`;
|
||||
} else if (entity.id) {
|
||||
entDetails = `${entity.id}`;
|
||||
}
|
||||
|
||||
if (feature && entDetails) {
|
||||
let featureName = getFeatureName({
|
||||
feature,
|
||||
plural: false,
|
||||
capitalize: true,
|
||||
});
|
||||
return `${featureName}: ${entDetails}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get entity invoice description`, { error });
|
||||
return "";
|
||||
}
|
||||
};
|
||||
@@ -134,17 +134,23 @@ export const getPricecnPrice = ({
|
||||
|
||||
if (isPriceItem(priceItem)) {
|
||||
return {
|
||||
...priceItem,
|
||||
primaryText: getPriceText({ item: priceItem, org }),
|
||||
secondaryText: priceItem.interval ? `per ${priceItem.interval}` : " ",
|
||||
};
|
||||
} else {
|
||||
let feature = features.find((f) => f.id == priceItem.feature_id);
|
||||
return featurePricetoPricecnItem({
|
||||
let texts = featurePricetoPricecnItem({
|
||||
feature,
|
||||
item: priceItem,
|
||||
org,
|
||||
isMainPrice,
|
||||
});
|
||||
return {
|
||||
...priceItem,
|
||||
primaryText: texts.primaryText,
|
||||
secondaryText: texts.secondaryText,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import Stripe from "stripe";
|
||||
import { constructPrice } from "../priceUtils.js";
|
||||
import { FullProduct, PriceType } from "@autumn/shared";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
import { subItemToAutumnInterval } from "tests/utils/stripeUtils.js";
|
||||
|
||||
export const subItemToFixedPrice = ({
|
||||
subItem,
|
||||
product,
|
||||
basePrice,
|
||||
}: {
|
||||
subItem: Stripe.SubscriptionItem;
|
||||
product: FullProduct;
|
||||
basePrice?: number;
|
||||
}) => {
|
||||
const { price } = subItem;
|
||||
|
||||
return constructPrice({
|
||||
internalProductId: product.internal_id,
|
||||
isCustom: true,
|
||||
orgId: product.org_id,
|
||||
fixedConfig: {
|
||||
type: PriceType.Fixed,
|
||||
amount: basePrice || (price.unit_amount || 0) / 100,
|
||||
interval: subItemToAutumnInterval(subItem)!,
|
||||
stripe_price_id: price.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -116,3 +116,13 @@ export const priceToProduct = ({
|
||||
(p: Product) => p.internal_id == price.internal_product_id,
|
||||
);
|
||||
};
|
||||
|
||||
export const filterByBillingType = ({
|
||||
prices,
|
||||
billingType,
|
||||
}: {
|
||||
prices: Price[];
|
||||
billingType: BillingType;
|
||||
}) => {
|
||||
return prices.filter((p) => getBillingType(p.config) == billingType);
|
||||
};
|
||||
|
||||
168
server/src/utils/importUtils/addProductFromSubs.ts
Normal file
168
server/src/utils/importUtils/addProductFromSubs.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import Stripe from "stripe";
|
||||
import { stripeToAutumnSubStatus } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.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";
|
||||
|
||||
export const addProductFromSubs = async ({
|
||||
req,
|
||||
autumnCus,
|
||||
autumnProduct,
|
||||
stripeSubs,
|
||||
prices,
|
||||
entitlements,
|
||||
force = false,
|
||||
isCustom = false,
|
||||
anchorToUnix,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
autumnCus: FullCustomer;
|
||||
autumnProduct: FullProduct;
|
||||
stripeSubs: Stripe.Subscription[];
|
||||
prices?: Price[];
|
||||
entitlements?: EntitlementWithFeature[];
|
||||
force?: boolean;
|
||||
isCustom?: boolean;
|
||||
anchorToUnix?: number;
|
||||
}) => {
|
||||
const { db, logger, org, env } = req;
|
||||
|
||||
const cusProducts = autumnCus.customer_products;
|
||||
const entity = autumnCus.entity;
|
||||
|
||||
let mainCusProduct = cusProducts.find(
|
||||
(cp) =>
|
||||
!cp.product.is_add_on &&
|
||||
cp.product_id == autumnProduct.id &&
|
||||
(notNullish(entity)
|
||||
? cp.internal_entity_id == entity!.internal_id
|
||||
: true),
|
||||
);
|
||||
|
||||
if (mainCusProduct && !force) {
|
||||
let prices = mainCusProduct.customer_prices.map((cp) => cp.price);
|
||||
let isFree = isFreeProduct(prices);
|
||||
|
||||
if (!isFree) {
|
||||
logger.info(
|
||||
`Customer ${
|
||||
autumnCus.id || autumnCus.email
|
||||
} already has non-free free product: ${
|
||||
mainCusProduct.product.name
|
||||
}, skipping...`,
|
||||
);
|
||||
return mainCusProduct;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle if trialing
|
||||
let trialEndsAt = stripeSubs[0].trial_end
|
||||
? stripeSubs[0].trial_end * 1000
|
||||
: null;
|
||||
|
||||
// 1. Insert custom prices...
|
||||
let customPrices = prices?.filter((p) => p.is_custom);
|
||||
if (customPrices && customPrices.length > 0) {
|
||||
await PriceService.upsert({
|
||||
db,
|
||||
data: customPrices,
|
||||
});
|
||||
}
|
||||
|
||||
let newCusProduct = await createFullCusProduct({
|
||||
db,
|
||||
attachParams: {
|
||||
replaceables: [],
|
||||
customer: autumnCus,
|
||||
product: autumnProduct,
|
||||
org,
|
||||
prices: notNullish(prices) ? prices! : autumnProduct.prices,
|
||||
entitlements: notNullish(entitlements)
|
||||
? entitlements!
|
||||
: autumnProduct.entitlements,
|
||||
freeTrial: autumnProduct.free_trial || null,
|
||||
optionsList: [],
|
||||
entities: [],
|
||||
cusProducts: cusProducts,
|
||||
features: [],
|
||||
internalEntityId: entity?.internal_id,
|
||||
entityId: entity?.id,
|
||||
isCustom: isCustom,
|
||||
},
|
||||
logger,
|
||||
trialEndsAt: trialEndsAt || undefined,
|
||||
subscriptionIds: stripeSubs.map((s) => s.id),
|
||||
anchorToUnix: anchorToUnix || stripeSubs[0].current_period_end * 1000,
|
||||
|
||||
subscriptionStatus: stripeToAutumnSubStatus(
|
||||
stripeSubs[0].status,
|
||||
) as CusProductStatus,
|
||||
|
||||
canceledAt: stripeSubs[0].canceled_at
|
||||
? stripeSubs[0].canceled_at * 1000
|
||||
: null,
|
||||
|
||||
createdAt: stripeSubs[0].created * 1000,
|
||||
sendWebhook: false,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Added product ${autumnProduct.name} to customer ${autumnCus.name}`,
|
||||
);
|
||||
|
||||
// Create sub
|
||||
let usageFeatures = autumnProduct.prices
|
||||
.filter((p) => isUsagePrice({ price: p }))
|
||||
.map((p) => (p.config as UsagePriceConfig).internal_feature_id);
|
||||
|
||||
for (const sub of stripeSubs) {
|
||||
let subFromDb = await SubService.getInStripeIds({
|
||||
db,
|
||||
ids: [sub.id],
|
||||
});
|
||||
|
||||
let subInterval = subToAutumnInterval(sub);
|
||||
|
||||
if (subFromDb.length === 0) {
|
||||
await SubService.createSub({
|
||||
db,
|
||||
sub: constructSub({
|
||||
stripeId: sub.id,
|
||||
usageFeatures:
|
||||
subInterval == BillingInterval.Month ? usageFeatures : [],
|
||||
orgId: org.id,
|
||||
env,
|
||||
currentPeriodStart: sub.current_period_start,
|
||||
currentPeriodEnd: sub.current_period_end,
|
||||
}),
|
||||
});
|
||||
logger.info(`Created sub ${sub.id} in DB`);
|
||||
} else {
|
||||
logger.info(`Sub ${sub.id} already exists in DB`);
|
||||
}
|
||||
}
|
||||
|
||||
autumnCus.customer_products = [
|
||||
...(autumnCus.customer_products || []),
|
||||
newCusProduct!,
|
||||
];
|
||||
|
||||
return newCusProduct;
|
||||
};
|
||||
52
server/src/utils/importUtils/addUsagePricesToSub.ts
Normal file
52
server/src/utils/importUtils/addUsagePricesToSub.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
|
||||
import { filterByBillingType } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
|
||||
import { BillingType, FullProduct, UsagePriceConfig } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const addContUsePricesToSub = async ({
|
||||
stripe,
|
||||
sub,
|
||||
autumnProduct,
|
||||
quantity,
|
||||
logger,
|
||||
}: {
|
||||
stripe: Stripe;
|
||||
sub: Stripe.Subscription;
|
||||
autumnProduct: FullProduct;
|
||||
quantity: number;
|
||||
logger: any;
|
||||
}) => {
|
||||
const usagePrices = filterByBillingType({
|
||||
prices: autumnProduct.prices,
|
||||
billingType: BillingType.InArrearProrated,
|
||||
});
|
||||
|
||||
logger.info(`Adding ${usagePrices.length} cont use prices to sub`);
|
||||
|
||||
for (const usagePrice of usagePrices) {
|
||||
const config = usagePrice.config as UsagePriceConfig;
|
||||
const latestSub = await stripe.subscriptions.retrieve(sub.id);
|
||||
let subItem = findStripeItemForPrice({
|
||||
price: usagePrice,
|
||||
stripeItems: latestSub.items.data,
|
||||
});
|
||||
|
||||
if (subItem) {
|
||||
logger.info(`Sub already has price for ${config.feature_id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newSubItem = await stripe.subscriptionItems.create({
|
||||
subscription: sub.id,
|
||||
price: usagePrice.config.stripe_price_id!,
|
||||
proration_behavior: "none",
|
||||
quantity,
|
||||
});
|
||||
|
||||
// logger.info(`New sub item:`, {
|
||||
// newSubItem,
|
||||
// });
|
||||
|
||||
logger.info(`Successfully added ${config.feature_id} to sub`);
|
||||
}
|
||||
};
|
||||
27
server/src/utils/importUtils/importUtils.ts
Normal file
27
server/src/utils/importUtils/importUtils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import Stripe from "stripe";
|
||||
import { subItemToFixedPrice } from "@/internal/products/prices/priceUtils/constructPriceUtils.js";
|
||||
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { FullProduct } from "@autumn/shared";
|
||||
|
||||
// Scenario 1: Replace base price with new base price
|
||||
export const replaceBasePrice = async ({
|
||||
subItems,
|
||||
autumnProduct,
|
||||
basePrice,
|
||||
}: {
|
||||
subItems: Stripe.SubscriptionItem[];
|
||||
autumnProduct: FullProduct;
|
||||
basePrice?: number;
|
||||
}) => {
|
||||
let prices = autumnProduct.prices.filter((p) => !isFixedPrice({ price: p }));
|
||||
|
||||
// Get first sub item
|
||||
const subItem = subItems[0];
|
||||
const customPrice = subItemToFixedPrice({
|
||||
subItem,
|
||||
product: autumnProduct,
|
||||
basePrice,
|
||||
});
|
||||
|
||||
return [customPrice, ...prices];
|
||||
};
|
||||
43
server/src/utils/importUtils/parseCsv.ts
Normal file
43
server/src/utils/importUtils/parseCsv.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { FeatureOptions } from "@autumn/shared";
|
||||
import csvParser from "csv-parser";
|
||||
import fs from "fs";
|
||||
|
||||
export interface ImportCustomer {
|
||||
id: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
stripe_id: string;
|
||||
product_id: string;
|
||||
base_price?: number;
|
||||
options?: FeatureOptions[];
|
||||
// business_id;name;email;active_pass_count;Stripe id;Base price;free_trial_end
|
||||
}
|
||||
export const parseCsv = (slug: string): Promise<any[]> => {
|
||||
const path = `scripts/customers/${slug}/data.csv`;
|
||||
const results: ImportCustomer[] = [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = fs.createReadStream(path);
|
||||
stream
|
||||
.pipe(csvParser({ separator: ";" }))
|
||||
.on("data", (data) =>
|
||||
results.push({
|
||||
id: data.business_id,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
stripe_id: data["Stripe id"],
|
||||
// base_price: data["Base price"],
|
||||
product_id: "standard_subscription",
|
||||
base_price: data["Base price"],
|
||||
options: [
|
||||
{
|
||||
feature_id: "active_passes",
|
||||
quantity: data["active_pass_count"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
.on("end", () => resolve(results))
|
||||
.on("error", (error) => reject(error));
|
||||
});
|
||||
};
|
||||
37
server/src/utils/importUtils/updateUsages.ts
Normal file
37
server/src/utils/importUtils/updateUsages.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { ACTIVE_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { cusProductsToCusEnts } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { CusProductStatus, FullCustomer } from "@autumn/shared";
|
||||
|
||||
export const updateUsages = async ({
|
||||
featureId,
|
||||
usage,
|
||||
fullCus,
|
||||
db,
|
||||
}: {
|
||||
featureId: string;
|
||||
usage: number;
|
||||
fullCus: FullCustomer;
|
||||
db: DrizzleCli;
|
||||
}) => {
|
||||
let cusEnts = cusProductsToCusEnts({
|
||||
cusProducts: fullCus.customer_products,
|
||||
inStatuses: ACTIVE_STATUSES,
|
||||
featureId,
|
||||
});
|
||||
if (cusEnts.length === 0) {
|
||||
throw new Error(`No cus ent for ${featureId}`);
|
||||
}
|
||||
|
||||
let cusEnt = cusEnts[0];
|
||||
let newBalance = cusEnt.balance! - usage;
|
||||
|
||||
await CusEntService.update({
|
||||
db,
|
||||
id: cusEnt.id,
|
||||
updates: {
|
||||
balance: newBalance,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -24,7 +24,8 @@ export const CreateCustomerSchema = z.object({
|
||||
name: z.string().nullish(),
|
||||
email: z.string().nullish(),
|
||||
fingerprint: z.string().nullish(),
|
||||
metadata: z.record(z.any()).nullish().default({}),
|
||||
metadata: z.record(z.any()).default({}).nullish(),
|
||||
stripe_id: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const CustomerDataSchema = z.object({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { invalidNumber, notNullish } from "@/utils/genUtils";
|
||||
import { Feature, FeatureUsageType, ProductItem } from "@autumn/shared";
|
||||
import { toast } from "sonner";
|
||||
import { isFeatureItem } from "../getItemType";
|
||||
import { isFeatureItem, isFeaturePriceItem } from "../getItemType";
|
||||
|
||||
export const validateProductItem = ({
|
||||
item,
|
||||
@@ -93,7 +93,11 @@ export const validateProductItem = ({
|
||||
toast.error("Please enter valid billing units");
|
||||
return null;
|
||||
} else {
|
||||
item.billing_units = Number(item.billing_units);
|
||||
if (isFeaturePriceItem(item)) {
|
||||
item.billing_units = Number(item.billing_units);
|
||||
} else {
|
||||
item.billing_units = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import LoadingScreen from "@/views/general/LoadingScreen";
|
||||
|
||||
import { useAxiosSWR } from "@/services/useAxiosSwr";
|
||||
@@ -19,7 +19,6 @@ import ProductSidebar from "./ProductSidebar";
|
||||
import { FeaturesContext } from "@/views/features/FeaturesContext";
|
||||
import ProductViewBreadcrumbs from "./components/ProductViewBreadcrumbs";
|
||||
import ConfirmNewVersionDialog from "./versioning/ConfirmNewVersionDialog";
|
||||
import { sortProductItems } from "@/utils/productUtils";
|
||||
import { useProductData } from "./hooks/useProductData";
|
||||
import { useProductChangedAlert } from "./hooks/useProductChangedAlert";
|
||||
|
||||
|
||||
@@ -63,18 +63,26 @@ export const ProductItemConfig = () => {
|
||||
});
|
||||
} else {
|
||||
const showProration = shouldShowProrationConfig({ item, features });
|
||||
const newConfig = !showProration ? null : item.config;
|
||||
const resetUsageWhenEnabled =
|
||||
feature.config?.usage_type == FeatureUsageType.Continuous
|
||||
? false
|
||||
: true;
|
||||
|
||||
setItem({
|
||||
const newItem = {
|
||||
...item,
|
||||
feature_type: feature.config?.usage_type,
|
||||
reset_usage_when_enabled: resetUsageWhenEnabled,
|
||||
config: newConfig,
|
||||
});
|
||||
};
|
||||
|
||||
const newConfig = !showProration ? undefined : item.config;
|
||||
|
||||
if (newConfig) {
|
||||
newItem.config = newConfig;
|
||||
} else {
|
||||
delete newItem.config;
|
||||
}
|
||||
|
||||
setItem(newItem);
|
||||
}
|
||||
}
|
||||
}, [item.feature_id, item.usage_model]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ProductItemConfig } from "./ProductItemConfig";
|
||||
import { ProductItem } from "@autumn/shared";
|
||||
import { ProductItemContext } from "./ProductItemContext";
|
||||
@@ -27,11 +27,12 @@ export default function UpdateProductItem({
|
||||
const { product, setProduct, features } = useProductContext();
|
||||
const [showCreateFeature, setShowCreateFeature] = useState(false);
|
||||
|
||||
const handleUpdateProductItem = (show: any) => {
|
||||
const handleUpdateProductItem = () => {
|
||||
const validatedItem = validateProductItem({
|
||||
item: selectedItem!,
|
||||
features,
|
||||
});
|
||||
|
||||
if (!validatedItem) return;
|
||||
if (notNullish(selectedIndex)) {
|
||||
const newProduct = { ...product };
|
||||
|
||||
@@ -34,10 +34,12 @@ export const IncludedUsage = () => {
|
||||
}
|
||||
type={item.included_usage === Infinite ? "text" : "number"}
|
||||
onChange={(e) => {
|
||||
setItem({
|
||||
const newItem = {
|
||||
...item,
|
||||
included_usage: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
setItem(newItem);
|
||||
}}
|
||||
/>
|
||||
<ToggleDisplayButton
|
||||
|
||||
@@ -66,7 +66,9 @@ export const BillingUnits = ({ disabled }: { disabled: boolean }) => {
|
||||
placeholder={`eg. 100 ${featureName}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleEnterClick();
|
||||
if (popoverOpen) {
|
||||
handleEnterClick();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={handleEnterClick}
|
||||
|
||||
Reference in New Issue
Block a user