moving cusproduct to tanstack

This commit is contained in:
John Yeo
2025-09-02 23:44:24 -07:00
parent f52c932200
commit 66cd5531fc
116 changed files with 1189 additions and 1105 deletions

View File

@@ -16,7 +16,7 @@ import {
} from "@autumn/shared";
import Stripe from "stripe";
import assert from "assert";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
import {
getAllStripeSchedules,

View File

@@ -1,9 +1,5 @@
import { isTrialing } from "@/internal/customers/cusProducts/cusProductUtils.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { priceToInvoiceDescription } from "@/internal/invoices/invoiceFormatUtils.js";
import { getProration } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { notNullish } from "@/utils/genUtils.js";
import {

View File

@@ -6,7 +6,7 @@ import {
activateDefaultProduct,
cancelCusProductSubscriptions,
} from "@/internal/customers/cusProducts/cusProductUtils.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {

View File

@@ -1,21 +1,13 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
AppEnv,
AttachScenario,
CusProductStatus,
Organization,
} from "@autumn/shared";
import Stripe from "stripe";
import { AttachScenario, CusProductStatus } from "@autumn/shared";
import { createStripeCli } from "../../utils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { cusProductInPhase } from "@/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.js";
import { formatUnixToDateTime, notNullish } from "@/utils/genUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { activateFutureProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";

View File

@@ -6,7 +6,7 @@ import {
cusProductToEnts,
cusProductToPrices,
cusProductToProduct,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
} from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { Entity, FullCusProduct, FullCustomer } from "@autumn/shared";
import Stripe from "stripe";

View File

@@ -11,7 +11,7 @@ import {
FullCustomerEntitlement,
EntInterval,
} from "@autumn/shared";
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
import { ACTIVE_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
import { isFreeProduct } from "../products/productUtils.js";

View File

@@ -23,11 +23,7 @@ import { ActionService } from "@/internal/analytics/ActionService.js";
import { constructAction } from "@/internal/analytics/actionUtils.js";
import { parseReqForAction } from "@/internal/analytics/actionUtils.js";
import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
import {
cusProductToEnts,
cusProductToPrices,
cusProductToProduct,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices, cusProductToProduct } from "@autumn/shared";
interface ActionDetails {
request_id: string;

View File

@@ -1,5 +1,5 @@
import { cusEntMatchesEntity } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import { cusProductsToCusEnts } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductsToCusEnts } from "@autumn/shared";
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js";
import { getCreditSystemsFromFeature } from "@/internal/features/creditSystemUtils.js";
import RecaseError from "@/utils/errorUtils.js";

View File

@@ -33,8 +33,7 @@ import { getCustomerSub } from "../../attachUtils/convertAttachParams.js";
import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js";
import { updateStripeSub2 } from "../upgradeFlow/updateStripeSub2.js";
import { subToNewSchedule } from "../../mergeUtils/subToNewSchedule.js";
import { isTrialing } from "@/internal/customers/cusProducts/cusProductUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { isTrialing } from "@autumn/shared";
export const handlePaidProduct = async ({
req,

View File

@@ -29,7 +29,7 @@ import {
import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js";
import { getAddAndRemoveProducts } from "./getAddAndRemoveProducts.js";
import { handleUpgradeFlowSchedule } from "../upgradeFlow/handleUpgradeFlowSchedule.js";
import { isTrialing } from "@/internal/customers/cusProducts/cusProductUtils.js";
import { isTrialing } from "@autumn/shared";
export const handleMultiAttachFlow = async ({
req,

View File

@@ -22,7 +22,7 @@ import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cus
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
import { notNullish } from "@/utils/genUtils.js";

View File

@@ -17,7 +17,7 @@ import {
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";

View File

@@ -7,10 +7,7 @@ import {
getCusPriceUsage,
getRelatedCusEnt,
} from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js";
import {
cusProductsToCusEnts,
cusProductsToCusPrices,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductsToCusEnts, cusProductsToCusPrices } from "@autumn/shared";
import {
formatPrice,
getBillingType,

View File

@@ -11,27 +11,16 @@ import {
} from "@autumn/shared";
import { logger } from "better-auth";
import Stripe from "stripe";
import { isTrialing } from "../../cusProducts/cusProductUtils.js";
import {
formatUnixToDate,
formatUnixToDateTime,
notNullish,
} from "@/utils/genUtils.js";
import { isTrialing } from "@autumn/shared";
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
import { priceToUsageModel } from "@/internal/products/prices/priceUtils/convertPrice.js";
import {
formatPrice,
getPriceEntitlement,
getPriceOptions,
} from "@/internal/products/prices/priceUtils.js";
import {
cusProductToEnts,
cusProductToProduct,
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToEnts } from "@autumn/shared";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
getAmountAfterStripeDiscounts,
getUnusedAmountAfterDiscount,
} from "@/internal/rewards/rewardUtils.js";
import { getUnusedAmountAfterDiscount } from "@/internal/rewards/rewardUtils.js";
import { Decimal } from "decimal.js";
const getDiscountsApplied = ({

View File

@@ -1,7 +1,4 @@
import {
cusProductToPrices,
cusProductToEnts,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices, cusProductToEnts } from "@autumn/shared";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { getEntsWithFeature } from "@/internal/products/entitlements/entitlementUtils.js";
import {

View File

@@ -6,7 +6,7 @@ import {
cusProductToEnts,
cusProductToPrices,
cusProductToProduct,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
} from "@autumn/shared";
import { newCusToFullCus } from "@/internal/customers/cusUtils/cusUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {

View File

@@ -11,7 +11,7 @@ import {
} from "./convertAttachParams.js";
import { FullCusProduct } from "@autumn/shared";
import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { isTrialing } from "../../cusProducts/cusProductUtils.js";
import { isTrialing } from "@autumn/shared";
import { ACTIVE_STATUSES } from "../../cusProducts/CusProductService.js";
export const getCycleWillReset = ({

View File

@@ -1,9 +1,7 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { CusProductStatus } from "@autumn/shared";
import { cusProductToProduct } from "@autumn/shared";
import Stripe from "stripe";
import { cusProductToProduct } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { isTrialing } from "../../cusProducts/cusProductUtils.js";
export const attachParamsToCurCusProduct = ({
attachParams,

View File

@@ -12,10 +12,7 @@ import {
isFreeProduct,
isProductUpgrade,
} from "@/internal/products/productUtils.js";
import {
cusProductToPrices,
cusProductToProduct,
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices, cusProductToProduct } from "@autumn/shared";
import { FeatureOptions, FullCusProduct } from "@autumn/shared";
import { productsAreSame } from "@/internal/products/productUtils/compareProductUtils.js";
import { hasPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";

View File

@@ -12,7 +12,7 @@ import { nullish, notNullish } from "@/utils/genUtils.js";
import { ProrationBehavior } from "@autumn/shared";
import { attachParamsToProduct } from "./convertAttachParams.js";
import { attachParamToCusProducts } from "./convertAttachParams.js";
import { cusProductToPrices } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { willMergeSub } from "../mergeUtils/mergeUtils.js";
export const intervalsAreSame = ({

View File

@@ -10,7 +10,7 @@ import {
import Stripe from "stripe";
import { getContUseInvoiceItems } from "./getContUseInvoiceItems.js";
import { findPriceInStripeItems } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { attachParamsToProduct } from "../convertAttachParams.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import { intervalsAreSame } from "../getAttachConfig.js";

View File

@@ -17,7 +17,7 @@ import { getContUseUpgradeItems } from "./getContUseUpgradeItems.js";
import { priceToInvoiceItem } from "@/internal/products/prices/priceUtils/priceToInvoiceItem.js";
import { Decimal } from "decimal.js";
import { notNullish } from "@/utils/genUtils.js";
import { cusProductsToCusPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductsToCusPrices } from "@autumn/shared";
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";

View File

@@ -20,10 +20,7 @@ import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { BillingType } from "@autumn/shared";
import { notNullish, nullOrUndefined } from "@/utils/genUtils.js";
import { attachParamToCusProducts } from "./convertAttachParams.js";
import {
cusProductsToCusEnts,
cusProductToPrices,
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductsToCusEnts, cusProductToPrices } from "@autumn/shared";
import { findPriceForFeature } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
import { getResetBalance } from "../../cusProducts/cusEnts/cusEntUtils.js";
import { Decimal } from "decimal.js";

View File

@@ -5,7 +5,7 @@ import {
attachParamsToCurCusProduct,
attachParamToCusProducts,
} from "../attachUtils/convertAttachParams.js";
import { cusProductToPrices } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { isFreeProduct } from "@/internal/products/productUtils.js";
export const getHasProrations = async ({

View File

@@ -15,7 +15,7 @@ import {
cusProductToEnts,
cusProductToPrices,
cusProductToProduct,
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
} from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
getProductItemResponse,

View File

@@ -5,7 +5,7 @@ import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
import { AttachFunction } from "@autumn/shared";
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
import { cusProductToProduct } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
import { attachParamToCusProducts } from "../attachUtils/convertAttachParams.js";
import { getDowngradeProductPreview } from "./getDowngradeProductPreview.js";
import { getNewProductPreview } from "./getNewProductPreview.js";

View File

@@ -1,9 +1,10 @@
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import { AttachBody, AttachBranch, PreviewLineItem } from "@autumn/shared";
import { cusProductsToPrices } from "@autumn/shared";
import { getCustomerSub } from "../attachUtils/convertAttachParams.js";
import { cusProductsToPrices } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { priceToUnusedPreviewItem } from "../attachPreviewUtils/priceToUnusedPreviewItem.js";
import { handleMultiAttachErrors } from "../attachUtils/handleAttachErrors/handleMultiAttachErrors.js";
import { getAddAndRemoveProducts } from "../attachFunctions/multiAttach/getAddAndRemoveProducts.js";

View File

@@ -24,7 +24,7 @@ import {
getSmallestInterval,
} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { isTrialing } from "../../cusProducts/cusProductUtils.js";
import { isTrialing } from "@autumn/shared";
const getNextCycleItems = async ({
newProduct,

View File

@@ -3,7 +3,7 @@ import {
attachParamsToProduct,
attachParamToCusProducts,
} from "../attachUtils/convertAttachParams.js";
import { cusProductToProduct } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
export const getUpdateEntsPreview = async ({
req,

View File

@@ -36,8 +36,8 @@ import {
getLatestPeriodEnd,
subToPeriodStartEnd,
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { isTrialing } from "../../cusProducts/cusProductUtils.js";
import { cusProductToPrices } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { isTrialing } from "@autumn/shared";
import { cusProductToPrices } from "@autumn/shared";
import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
const getNextCycleAt = ({

View File

@@ -10,7 +10,7 @@ import {
import { AttachBranch, Entity, FullCusProduct, Price } from "@autumn/shared";
import Stripe from "stripe";
import { getExistingUsageFromCusProducts } from "../../cusProducts/cusEnts/cusEntUtils.js";
import { cusProductToEnts } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToEnts } from "@autumn/shared";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
attachParamsToCurCusProduct,

View File

@@ -6,7 +6,7 @@ import { AttachParams } from "../../cusProducts/AttachParams.js";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { AttachConfig, FullCusProduct } from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { cusProductToPrices } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { isArrearPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
priceToScheduleItem,

View File

@@ -4,10 +4,7 @@ import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSu
import { AttachConfig, CusProductStatus, FullCusProduct } from "@autumn/shared";
import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
cusProductToPrices,
cusProductToProduct,
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices, cusProductToProduct } from "@autumn/shared";
import { isArrearPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
findStripeItemForPrice,

View File

@@ -6,16 +6,15 @@ import {
FullCusProduct,
FullCustomer,
} from "@autumn/shared";
import {
cusProductToProduct,
cusProductToSub,
} from "../cusProducts/cusProductUtils/convertCusProduct.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { cusProductToProduct } from "@autumn/shared";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { activateDefaultProduct } from "../cusProducts/cusProductUtils.js";
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js";
export const cancelImmediately = async ({
req,

View File

@@ -11,10 +11,7 @@ import {
EntitlementWithFeature,
} from "@autumn/shared";
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
import {
cusProductToPrices,
cusProductToProduct,
} from "../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices, cusProductToProduct } from "@autumn/shared";
import { createStripeCli } from "@/external/stripe/utils.js";
import RecaseError from "@/utils/errorUtils.js";
import { StatusCodes } from "http-status-codes";

View File

@@ -118,131 +118,131 @@ export const getCusEntBalance = ({
};
};
export const sortCusEntsForDeduction = (
cusEnts: (FullCustomerEntitlement & {
customer_product?: FullCusProduct;
})[],
reverseOrder: boolean = false
) => {
let intervalOrder: Record<EntInterval, number> = {
[EntInterval.Minute]: 0, // 1 minute
[EntInterval.Hour]: 1, // 1 hour
[EntInterval.Day]: 2, // 1 day
[EntInterval.Week]: 3, // 1 week
[EntInterval.Month]: 4, // 1 month
[EntInterval.Quarter]: 5, // 3 months
[EntInterval.Year]: 6, // 1 year
[EntInterval.SemiAnnual]: 7, // 6 months
[EntInterval.Lifetime]: 8, // 1 time
};
// export const sortCusEntsForDeduction = (
// cusEnts: (FullCustomerEntitlement & {
// customer_product?: FullCusProduct;
// })[],
// reverseOrder: boolean = false
// ) => {
// let intervalOrder: Record<EntInterval, number> = {
// [EntInterval.Minute]: 0, // 1 minute
// [EntInterval.Hour]: 1, // 1 hour
// [EntInterval.Day]: 2, // 1 day
// [EntInterval.Week]: 3, // 1 week
// [EntInterval.Month]: 4, // 1 month
// [EntInterval.Quarter]: 5, // 3 months
// [EntInterval.Year]: 6, // 1 year
// [EntInterval.SemiAnnual]: 7, // 6 months
// [EntInterval.Lifetime]: 8, // 1 time
// };
// console.log(
// `Cus ents before (${reverseOrder ? "reversed" : "normal"})`,
// cusEnts.map(
// (ce) => `${ce.entitlement.feature_id} - ${ce.entitlement.interval}`
// )
// );
cusEnts.sort((a, b) => {
const aEnt = a.entitlement;
const bEnt = b.entitlement;
// // console.log(
// // `Cus ents before (${reverseOrder ? "reversed" : "normal"})`,
// // cusEnts.map(
// // (ce) => `${ce.entitlement.feature_id} - ${ce.entitlement.interval}`
// // )
// // );
// cusEnts.sort((a, b) => {
// const aEnt = a.entitlement;
// const bEnt = b.entitlement;
// 1. If boolean, go first
if (aEnt.feature.type == FeatureType.Boolean) {
return -1;
}
// // 1. If boolean, go first
// if (aEnt.feature.type == FeatureType.Boolean) {
// return -1;
// }
if (bEnt.feature.type == FeatureType.Boolean) {
return 1;
}
// if (bEnt.feature.type == FeatureType.Boolean) {
// return 1;
// }
// 1. If a is credit system and b is not, a should go last
if (
aEnt.feature.type == FeatureType.CreditSystem &&
bEnt.feature.type != FeatureType.CreditSystem
) {
return 1;
}
// // 1. If a is credit system and b is not, a should go last
// if (
// aEnt.feature.type == FeatureType.CreditSystem &&
// bEnt.feature.type != FeatureType.CreditSystem
// ) {
// return 1;
// }
// 2. If a is not credit system and b is, a should go first
if (
aEnt.feature.type != FeatureType.CreditSystem &&
bEnt.feature.type == FeatureType.CreditSystem
) {
return -1;
}
// // 2. If a is not credit system and b is, a should go first
// if (
// aEnt.feature.type != FeatureType.CreditSystem &&
// bEnt.feature.type == FeatureType.CreditSystem
// ) {
// return -1;
// }
// 2. Sort by unlimited (unlimited goes first)
if (
aEnt.allowance_type == AllowanceType.Unlimited &&
bEnt.allowance_type != AllowanceType.Unlimited
) {
return -1;
}
// // 2. Sort by unlimited (unlimited goes first)
// if (
// aEnt.allowance_type == AllowanceType.Unlimited &&
// bEnt.allowance_type != AllowanceType.Unlimited
// ) {
// return -1;
// }
if (
aEnt.allowance_type != AllowanceType.Unlimited &&
bEnt.allowance_type == AllowanceType.Unlimited
) {
return 1;
}
// if (
// aEnt.allowance_type != AllowanceType.Unlimited &&
// bEnt.allowance_type == AllowanceType.Unlimited
// ) {
// return 1;
// }
// If one has usage_allowed, it should go last
if (!a.usage_allowed && b.usage_allowed) {
return -1;
}
// // If one has usage_allowed, it should go last
// if (!a.usage_allowed && b.usage_allowed) {
// return -1;
// }
if (!b.usage_allowed && a.usage_allowed) {
return 1;
}
// if (!b.usage_allowed && a.usage_allowed) {
// return 1;
// }
// If one has a next_reset_at, it should go first
let nextResetFirst = reverseOrder ? 1 : -1;
// // If one has a next_reset_at, it should go first
// let nextResetFirst = reverseOrder ? 1 : -1;
if (a.next_reset_at && !b.next_reset_at) {
return nextResetFirst;
}
// if (a.next_reset_at && !b.next_reset_at) {
// return nextResetFirst;
// }
// If b has a next_reset_at, it should go first
if (!a.next_reset_at && b.next_reset_at) {
return -nextResetFirst;
}
// // If b has a next_reset_at, it should go first
// if (!a.next_reset_at && b.next_reset_at) {
// return -nextResetFirst;
// }
// 3. Sort by interval
let aVal = entIntervalToValue(aEnt.interval, aEnt.interval_count);
let bVal = entIntervalToValue(bEnt.interval, bEnt.interval_count);
if (aEnt.interval && bEnt.interval && !aVal.eq(bVal)) {
if (reverseOrder) {
return bVal.sub(aVal).toNumber();
// return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval];
} else {
return aVal.sub(bVal).toNumber();
// return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval];
}
}
// // 3. Sort by interval
// let aVal = entIntervalToValue(aEnt.interval, aEnt.interval_count);
// let bVal = entIntervalToValue(bEnt.interval, bEnt.interval_count);
// if (aEnt.interval && bEnt.interval && !aVal.eq(bVal)) {
// if (reverseOrder) {
// return bVal.sub(aVal).toNumber();
// // return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval];
// } else {
// return aVal.sub(bVal).toNumber();
// // return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval];
// }
// }
// Check if a is main product
let aIsAddOn = a.customer_product?.product?.is_add_on;
let bIsAddOn = b.customer_product?.product?.is_add_on;
// // Check if a is main product
// let aIsAddOn = a.customer_product?.product?.is_add_on;
// let bIsAddOn = b.customer_product?.product?.is_add_on;
if (aIsAddOn && !bIsAddOn) {
return 1;
}
// if (aIsAddOn && !bIsAddOn) {
// return 1;
// }
if (!aIsAddOn && bIsAddOn) {
return -1;
}
// if (!aIsAddOn && bIsAddOn) {
// return -1;
// }
// 4. Sort by created_at
return a.created_at - b.created_at;
});
// // 4. Sort by created_at
// return a.created_at - b.created_at;
// });
// console.log(
// `Cus ents after (${reverseOrder ? "reversed" : "normal"})`,
// cusEnts.map(
// (ce) => `${ce.entitlement.feature_id} - ${ce.entitlement.interval}`
// )
// );
};
// // console.log(
// // `Cus ents after (${reverseOrder ? "reversed" : "normal"})`,
// // cusEnts.map(
// // (ce) => `${ce.entitlement.feature_id} - ${ce.entitlement.interval}`
// // )
// // );
// };
// Get related cusPrice
export const getRelatedCusPrice = (

View File

@@ -15,11 +15,11 @@ import {
getRelatedCusPrice,
getResetBalance,
getUnlimitedAndUsageAllowed,
sortCusEntsForDeduction,
} from "../cusEntUtils.js";
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js";
import { sortCusEntsForDeduction } from "@autumn/shared";
export const getExistingCusEntAndUsage = async ({
curCusProduct,

View File

@@ -36,12 +36,9 @@ import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/han
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getExistingCusProducts } from "./cusProductUtils/getExistingCusProducts.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { cusProductToPrices } from "./cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import {
isDefaultTrial,
isDefaultTrialFullProduct,
} from "@/internal/products/productUtils/classifyProduct.js";
import { isDefaultTrialFullProduct } from "@/internal/products/productUtils/classifyProduct.js";
import { initStripeCusAndProducts } from "../handlers/handleCreateCustomer.js";
import { handleAddProduct } from "../attach/attachFunctions/addProductFlow/handleAddProduct.js";
import { newCusToAttachParams } from "../attach/attachUtils/attachParams/convertToParams.js";
@@ -603,18 +600,6 @@ export const searchCusProducts = ({
});
};
export const isTrialing = ({
cusProduct,
now,
}: {
cusProduct: FullCusProduct;
now?: number;
}) => {
return (
cusProduct.trial_ends_at && cusProduct.trial_ends_at > (now || Date.now())
);
};
export const getMainCusProduct = async ({
db,
internalCustomerId,

View File

@@ -1,6 +1,7 @@
import {
BillingType,
CusProductStatus,
cusProductToPrices,
FullCusEntWithFullCusProduct,
FullCusProduct,
FullCustomerEntitlement,
@@ -8,128 +9,125 @@ import {
FullProduct,
Product,
} from "@autumn/shared";
import { sortCusEntsForDeduction } from "../cusEnts/cusEntUtils.js";
import { sortCusEntsForDeduction } from "@autumn/shared";
import {
getStripeSchedules,
getStripeSubs,
} from "@/external/stripe/stripeSubUtils.js";
import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { ACTIVE_STATUSES } from "../CusProductService.js";
export const cusProductsToPrices = ({
cusProducts,
}: {
cusProducts: FullCusProduct[];
}) => {
return cusProducts.flatMap((cp) => cusProductToPrices({ cusProduct: cp }));
};
// export const cusProductsToPrices = ({
// cusProducts,
// }: {
// cusProducts: FullCusProduct[];
// }) => {
// return cusProducts.flatMap((cp) => cusProductToPrices({ cusProduct: cp }));
// };
export const cusProductsToCusPrices = ({
cusProducts,
inStatuses,
billingType,
}: {
cusProducts: FullCusProduct[];
inStatuses?: CusProductStatus[];
billingType?: BillingType;
}) => {
const cusPrices: FullCustomerPrice[] = [];
// export const cusProductsToCusPrices = ({
// cusProducts,
// inStatuses,
// billingType,
// }: {
// cusProducts: FullCusProduct[];
// inStatuses?: CusProductStatus[];
// billingType?: BillingType;
// }) => {
// const cusPrices: FullCustomerPrice[] = [];
for (const cusProduct of cusProducts) {
if (inStatuses && !inStatuses.includes(cusProduct.status)) {
continue;
}
// for (const cusProduct of cusProducts) {
// if (inStatuses && !inStatuses.includes(cusProduct.status)) {
// continue;
// }
let prices = cusProduct.customer_prices;
if (billingType) {
prices = prices.filter(
(cp) => getBillingType(cp.price.config) === billingType
);
}
// let prices = cusProduct.customer_prices;
// if (billingType) {
// prices = prices.filter(
// (cp) => getBillingType(cp.price.config) === billingType
// );
// }
cusPrices.push(...prices);
}
// cusPrices.push(...prices);
// }
return cusPrices;
};
// return cusPrices;
// };
export const cusProductsToCusEnts = ({
cusProducts,
inStatuses = [CusProductStatus.Active],
reverseOrder = false,
featureId,
}: {
cusProducts: FullCusProduct[];
inStatuses?: CusProductStatus[];
reverseOrder?: boolean;
featureId?: string;
}) => {
let cusEnts: FullCustomerEntitlement[] = [];
// export const cusProductsToCusEnts = ({
// cusProducts,
// inStatuses = [CusProductStatus.Active],
// reverseOrder = false,
// featureId,
// }: {
// cusProducts: FullCusProduct[];
// inStatuses?: CusProductStatus[];
// reverseOrder?: boolean;
// featureId?: string;
// }) => {
// let cusEnts: FullCustomerEntitlement[] = [];
for (const cusProduct of cusProducts) {
if (!inStatuses.includes(cusProduct.status)) {
continue;
}
// for (const cusProduct of cusProducts) {
// if (!inStatuses.includes(cusProduct.status)) {
// continue;
// }
cusEnts.push(
...cusProduct.customer_entitlements.map((cusEnt) => ({
...cusEnt,
customer_product: cusProduct,
}))
);
}
// cusEnts.push(
// ...cusProduct.customer_entitlements.map((cusEnt) => ({
// ...cusEnt,
// customer_product: cusProduct,
// }))
// );
// }
if (featureId) {
cusEnts = cusEnts.filter(
(cusEnt) => cusEnt.entitlement.feature_id === featureId
);
}
// if (featureId) {
// cusEnts = cusEnts.filter(
// (cusEnt) => cusEnt.entitlement.feature_id === featureId
// );
// }
sortCusEntsForDeduction(cusEnts, reverseOrder);
// sortCusEntsForDeduction(cusEnts, reverseOrder);
return cusEnts as FullCusEntWithFullCusProduct[];
};
// return cusEnts as FullCusEntWithFullCusProduct[];
// };
export const cusProductToPrices = ({
cusProduct,
billingType,
}: {
cusProduct: FullCusProduct;
billingType?: BillingType;
}) => {
let prices = cusProduct.customer_prices.map((cp) => cp.price);
// export const cusProductToPrices = ({
// cusProduct,
// billingType,
// }: {
// cusProduct: FullCusProduct;
// billingType?: BillingType;
// }) => {
// let prices = cusProduct.customer_prices.map((cp) => cp.price);
if (billingType) {
prices = prices.filter((p) => getBillingType(p.config) === billingType);
}
// if (billingType) {
// prices = prices.filter((p) => getBillingType(p.config) === billingType);
// }
return prices;
};
// return prices;
// };
export const cusProductToEnts = ({
cusProduct,
}: {
cusProduct: FullCusProduct;
}) => {
return cusProduct.customer_entitlements.map((ce) => ce.entitlement);
};
// export const cusProductToEnts = ({
// cusProduct,
// }: {
// cusProduct: FullCusProduct;
// }) => {
// return cusProduct.customer_entitlements.map((ce) => ce.entitlement);
// };
export const cusProductToProduct = ({
cusProduct,
}: {
cusProduct: FullCusProduct;
}) => {
return {
...cusProduct.product,
prices: cusProductToPrices({ cusProduct }),
entitlements: cusProductToEnts({ cusProduct }),
free_trial: cusProduct.free_trial,
} as FullProduct;
};
// export const cusProductToProduct = ({
// cusProduct,
// }: {
// cusProduct: FullCusProduct;
// }) => {
// return {
// ...cusProduct.product,
// prices: cusProductToPrices({ cusProduct }),
// entitlements: cusProductToEnts({ cusProduct }),
// free_trial: cusProduct.free_trial,
// } as FullProduct;
// };
// Subs, schedules
export const cusProductsToSchedules = ({
cusProducts,
stripeCli,

View File

@@ -1,6 +1,6 @@
import { formatUnixToDateTime } from "@/utils/genUtils.js";
import { CusProductStatus, FullCusProduct } from "@autumn/shared";
import { cusProductToPrices } from "./convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
export type ScheduledStartStatusPair = {

View File

@@ -5,12 +5,8 @@ import {
Entity,
APIVersion,
} from "@autumn/shared";
import {
cusProductsToCusEnts,
cusProductsToCusPrices,
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductsToCusEnts, cusProductsToCusPrices } from "@autumn/shared";
import { getCusBalances } from "./getCusBalances.js";
import { ACTIVE_STATUSES } from "../../cusProducts/CusProductService.js";
export const getCusFeaturesResponse = async ({
cusProducts,

View File

@@ -9,12 +9,12 @@ import {
Invoice,
InvoiceResponse,
Organization,
sortCusEntsForDeduction,
} from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { sortCusEntsForDeduction } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { StatusCodes } from "http-status-codes";
import { notNullish, nullish } from "@/utils/genUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";

View File

@@ -19,10 +19,8 @@ import { getCusInvoices } from "./cusUtils.js";
import { orgToVersion } from "@/utils/versionUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
cusProductsToCusEnts,
cusProductsToCusPrices,
} from "../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductsToCusEnts, cusProductsToCusPrices } from "@autumn/shared";
import { invoicesToResponse } from "@/internal/invoices/invoiceUtils.js";
import { getCusBalances } from "./cusFeatureResponseUtils/getCusBalances.js";
import { featuresToObject } from "./cusFeatureResponseUtils/balancesToFeatureResponse.js";

View File

@@ -23,7 +23,7 @@ import {
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { CusService } from "../CusService.js";
import { cusProductToPrices } from "../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
export const expireCusProduct = async ({
req,

View File

@@ -8,7 +8,7 @@ import {
} from "@autumn/shared";
import { createFullCusProduct } from "../../add-product/createFullCusProduct.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import { cusProductToProduct } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import RecaseError from "@/utils/errorUtils.js";

View File

@@ -9,6 +9,7 @@ import {
FullCusProduct,
FullCustomerEntitlement,
FullCustomerPrice,
productToCusProduct,
} from "@autumn/shared";
import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
@@ -22,11 +23,12 @@ import { mapToProductV2 } from "../products/productV2Utils.js";
import { RewardRedemptionService } from "../rewards/RewardRedemptionService.js";
import { CusReadService } from "./CusReadService.js";
import { StatusCodes } from "http-status-codes";
import { cusProductToProduct } from "./cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
import { createOrgResponse, isStripeConnected } from "../orgs/orgUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { CusSearchService } from "./CusSearchService.js";
import { CusBatchService } from "../api/batch/CusBatchService.js";
import { ACTIVE_STATUSES } from "./cusProducts/CusProductService.js";
export const cusRouter: Router = Router();
@@ -127,203 +129,6 @@ cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
}
});
// cusRouter.get("/:customer_id/stripe", async (req: any, res: any) => {
// try {
// const { db, org, features, env } = req;
// const { customer_id } = req.params;
// let discount = null;
// const customer = await CusService.get({
// db,
// orgId: req.orgId,
// env,
// idOrInternalId: customer_id,
// });
// if (org.stripe_config && customer.processor?.id) {
// try {
// const stripeCli = createStripeCli({ org, env });
// const stripeCus: any = await stripeCli.customers.retrieve(
// customer.processor.id
// );
// if (stripeCus.discount) {
// discount = stripeCus.discount;
// }
// } catch (error) {
// console.log("error", error);
// }
// }
// } catch (error) {
// handleFrontendReqError({ req, error, res, action: "get customer data" });
// }
// });
// cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
// try {
// const { db, org, features, env } = req;
// const { customer_id } = req.params;
// const orgId = req.orgId;
// const limit = req.query.limit || 10;
// const period = req.query.period || "all";
// console.log("Fetching events for customer:", customer_id);
// const events = await EventService.getByCustomerId({
// db,
// internalCustomerId: customer_id,
// env,
// orgId: orgId,
// limit,
// });
// console.log("Events:", events);
// res.status(200).json({ events });
// } catch (error) {
// handleFrontendReqError({ req, error, res, action: "get customer events" });
// }
// });
cusRouter.get("/:customer_id/data", async (req: any, res: any) => {
try {
const { db, org, features, env } = req;
const { customer_id } = req.params;
const orgId = req.orgId;
const [coupons, products, customer] = await Promise.all([
RewardService.list({
db,
orgId: orgId,
env,
}),
ProductService.listFull({ db, orgId, env, returnAll: true }),
CusService.getFull({
db,
orgId,
env,
idOrInternalId: customer_id,
withEntities: true,
expand: [CusExpand.Invoices],
inStatuses: [
CusProductStatus.Active,
CusProductStatus.PastDue,
CusProductStatus.Scheduled,
CusProductStatus.Expired,
],
}),
]);
if (!customer) {
throw new RecaseError({
message: "Customer not found",
code: ErrCode.CustomerNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
let invoices = customer.invoices;
let entities = customer.entities;
const events = await EventService.getByCustomerId({
db,
internalCustomerId: customer.internal_id,
env,
orgId: orgId,
limit: 10,
});
let fullCustomer = customer as any;
let cusProducts = fullCustomer.customer_products;
fullCustomer.products = fullCustomer.customer_products;
fullCustomer.entitlements = cusProducts.flatMap(
(product: FullCusProduct) => product.customer_entitlements
);
fullCustomer.prices = cusProducts.flatMap(
(product: FullCusProduct) => product.customer_prices
);
for (const product of fullCustomer.products) {
product.entitlements = product.customer_entitlements.map(
(cusEnt: FullCustomerEntitlement) => {
return cusEnt.entitlement;
}
);
product.prices = product.customer_prices.map(
(cusPrice: FullCustomerPrice) => {
return cusPrice.price;
}
);
}
let discount = null;
if (org.stripe_config && customer.processor?.id) {
try {
const stripeCli = createStripeCli({ org, env });
const stripeCus: any = await stripeCli.customers.retrieve(
customer.processor.id
);
if (stripeCus.discount) {
discount = stripeCus.discount;
}
} catch (error) {
console.log("error", error);
}
}
for (const invoice of invoices || []) {
invoice.product_ids = invoice.product_ids.sort();
invoice.internal_product_ids = invoice.internal_product_ids.sort();
}
// fullCustomer.entitlements = fullCustomer.entitlements.sort(
// (a: any, b: any) => {
// const productA = fullCustomer.products.find(
// (p: any) => p.id === a.customer_product_id
// );
// const productB = fullCustomer.products.find(
// (p: any) => p.id === b.customer_product_id
// );
// return (
// new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ||
// b.id.localeCompare(a.id)
// );
// }
// );
// for (const cusEnt of fullCustomer.entitlements) {
// // let entitlement = cusEnt.entitlement;
// // Show used, limit, etc.
// let { balance, unused } = getCusEntMasterBalance({
// cusEnt,
// entities,
// });
// cusEnt.balance = balance;
// cusEnt.unused = unused;
// }
res.status(200).json({
customer: fullCustomer,
products: getLatestProducts(products),
versionCounts: getProductVersionCounts(products),
invoices,
features,
coupons,
events,
discount,
org,
entities,
});
} catch (error) {
handleFrontendReqError({ req, error, res, action: "get customer data" });
}
});
cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => {
try {
const { env, db, org } = req;
@@ -476,34 +281,15 @@ cusRouter.get(
let cusProducts = customer.customer_products;
let entity = customer.entity;
let cusProduct;
if (notNullish(customer_product_id)) {
cusProduct = cusProducts.find(
(p: any) =>
p.id === customer_product_id &&
(entity ? p.internal_entity_id === entity.internal_id : true)
);
} else if (notNullish(version)) {
cusProduct = cusProducts.find(
(p: any) =>
p.product.id === product_id &&
(p.status === CusProductStatus.Active ||
p.status === CusProductStatus.PastDue) &&
p.product.version === parseInt(version) &&
(entity ? p.internal_entity_id === entity.internal_id : true)
);
} else {
cusProduct = cusProducts.find(
(p: any) =>
p.product.id === product_id &&
(p.status === CusProductStatus.Active ||
p.status === CusProductStatus.PastDue) &&
(entity
? p.internal_entity_id === entity.internal_id
: nullish(p.internal_entity_id))
);
}
let cusProduct = productToCusProduct({
cusProducts,
productId: product_id,
internalEntityId: entity?.internal_id,
version: version ? parseInt(version) : undefined,
cusProductId: customer_product_id,
inStatuses: ACTIVE_STATUSES,
});
let product = cusProduct
? cusProductToProduct({ cusProduct })
@@ -519,32 +305,32 @@ cusRouter.get(
});
let productV2 = mapToProductV2({ product: product!, features });
let numVersions = await ProductService.getProductVersionCount({
db,
orgId: org.id,
env,
productId: product_id,
});
// let numVersions = await ProductService.getProductVersionCount({
// db,
// orgId: org.id,
// env,
// productId: product_id,
// });
res.status(200).json({
customer,
// preview,
product: cusProduct
? {
...productV2,
options: cusProduct.options,
isActive: cusProduct.status === CusProductStatus.Active,
isCustom: cusProduct.is_custom,
isCanceled:
cusProduct.canceled_at !== null || cusProduct.canceled,
cusProductId: cusProduct.id,
}
: productV2,
features,
numVersions,
entities: customer.entities,
org: createOrgResponse({ org, env }),
cusProduct,
product: productV2,
// customer,
// product: cusProduct
// ? {
// ...productV2,
// options: cusProduct.options,
// isActive: cusProduct.status === CusProductStatus.Active,
// isCustom: cusProduct.is_custom,
// isCanceled:
// cusProduct.canceled_at !== null || cusProduct.canceled,
// cusProductId: cusProduct.id,
// }
// : productV2,
// features,
// numVersions,
// entities: customer.entities,
// org: createOrgResponse({ org, env }),
});
} catch (error) {
handleFrontendReqError({

View File

@@ -18,10 +18,7 @@ import { generateFeatureDisplay } from "@/external/llm/llmUtils.js";
import { ProductService } from "../products/ProductService.js";
import { getCreditSystemsFromFeature } from "./creditSystemUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
cusProductsToCusPrices,
cusProductToPrices,
} from "../customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductsToCusPrices, cusProductToPrices } from "@autumn/shared";
import { priceToFeature } from "../products/prices/priceUtils/convertPrice.js";
import { ACTIVE_STATUSES } from "../customers/cusProducts/CusProductService.js";

View File

@@ -18,11 +18,8 @@ import {
getBillingType,
getPriceEntitlement,
} from "../products/prices/priceUtils.js";
import { cusProductToEnts } from "../customers/cusProducts/cusProductUtils/convertCusProduct.js";
import {
getCusPriceUsage,
getRelatedCusEnt,
} from "../customers/cusProducts/cusPrices/cusPriceUtils.js";
import { cusProductToEnts } from "@autumn/shared";
import { getFeatureQuantity } from "../customers/cusProducts/cusProductUtils.js";
import { formatAmount } from "@/utils/formatUtils.js";
import { getIntervalString } from "../products/productUtils/productResponseUtils/getProductItemDisplay.js";

View File

@@ -19,10 +19,7 @@ import {
priceToUsageModel,
} from "@/internal/products/prices/priceUtils/convertPrice.js";
import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
import {
cusProductToEnts,
cusProductToPrices,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToEnts, cusProductToPrices } from "@autumn/shared";
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";

View File

@@ -1,7 +1,7 @@
import Stripe from "stripe";
import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";

View File

@@ -119,6 +119,42 @@ productRouter.get("/rewards", async (req: any, res) => {
}
});
productRouter.get("/:productId/data2", async (req: any, res) => {
try {
const { productId } = req.params;
const { version } = req.query;
const { db, orgId, env } = req;
const product = await ProductService.getFull({
db,
idOrInternalId: productId,
orgId,
env,
version: version ? parseInt(version) : undefined,
});
if (!product) {
throw new RecaseError({
message: `Product ${productId} ${
version ? `(v${version})` : ""
} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
let productV2 = mapToProductV2({
product: product,
features: req.features,
});
res.status(200).json({ product: productV2 });
} catch (error) {
console.error("Failed to get product", error);
res.status(500).send(error);
}
});
productRouter.get("/data", async (req: any, res) => {
try {
let { db } = req;

View File

@@ -16,11 +16,10 @@ import {
AttachScenario,
FullProduct,
FullCustomer,
FreeTrialResponseSchema,
} from "@autumn/shared";
import { isPriceItem } from "../product-items/productItemUtils/getItemType.js";
import { isFeaturePriceItem } from "../product-items/productItemUtils/getItemType.js";
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
import { isProductUpgrade } from "../productUtils.js";
import { getLargestInterval } from "../prices/priceUtils/priceIntervalUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";

View File

@@ -1,4 +1,4 @@
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToProduct } from "@autumn/shared";
import { AttachScenario, FullCustomer, FullProduct } from "@autumn/shared";
import {
isFreeProduct,

View File

@@ -23,7 +23,7 @@ import {
cusProductToPrices,
cusProductToEnts,
cusProductToProduct,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
} from "@autumn/shared";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import {
getPriceEntitlement,

View File

@@ -9,7 +9,7 @@ import {
} from "@autumn/shared";
import Stripe from "stripe";
import { ExtendedRequest } from "../models/Request.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { getUsageBasedSub } from "@/external/stripe/stripeSubUtils.js";
import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js";
import { logger } from "@/external/logtail/logtailUtils.js";

View File

@@ -1,7 +1,7 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
import { cusProductsToCusEnts } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductsToCusEnts } from "@autumn/shared";
import { FullCustomer } from "@autumn/shared";
export const updateUsages = async ({

View File

@@ -28,13 +28,7 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addWeeks } from "date-fns";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { timeout } from "@/utils/genUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { isPrepaidPrice } from "@shared/utils/productUtils/priceUtils.js";
import { isContUsePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import { Decimal } from "decimal.js";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
let premium = constructProduct({

View File

@@ -26,17 +26,6 @@ import {
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addWeeks } from "date-fns";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { timeout } from "@/utils/genUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { isPrepaidPrice } from "@shared/utils/productUtils/priceUtils.js";
import { isContUsePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import { Decimal } from "decimal.js";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
let premium = constructProduct({
id: "premium",

View File

@@ -5,7 +5,7 @@ import {
cusProductToEnts,
cusProductToPrices,
cusProductToProduct,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
} from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import {
getPriceEntitlement,

View File

@@ -15,7 +15,7 @@ import {
cusProductToPrices,
cusProductToEnts,
cusProductToProduct,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
} from "@autumn/shared";
import {
formatPrice,
getPriceEntitlement,

View File

@@ -2,7 +2,7 @@ import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import { findContUsePrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
import { AppEnv, FullCustomer, Organization } from "@autumn/shared";
@@ -10,7 +10,7 @@ import { expect } from "chai";
import { TestFeature } from "tests/setup/v2Features.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const expectSubQuantityCorrect = async ({

View File

@@ -1,8 +1,5 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
cusProductToPrices,
cusProductToEnts,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices, cusProductToEnts } from "@autumn/shared";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import {

View File

@@ -2,7 +2,7 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js";
import { findStripePriceFromPrices } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import { isV4Usage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {

View File

@@ -4,10 +4,7 @@ import {
findStripeItemForPrice,
isLicenseItem,
} from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import {
cusProductToPrices,
cusProductToSub,
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { cusProductToPrices } from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { isV4Usage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";

View File

@@ -145,6 +145,7 @@ export * from "./utils/productUtils.js";
export * from "./utils/productDisplayUtils/sortProductItems.js";
export * from "./utils/intervalUtils.js";
export * from "./utils/productUtils/priceToInvoiceAmount.js";
export * from "./utils/index.js";
// ENUMS
export * from "./enums/SuccessCode.js";

View File

@@ -0,0 +1,113 @@
import { FullCustomerEntitlement } from "../../models/cusProductModels/cusEntModels/cusEntModels.js";
import { FullCusProduct } from "../../models/cusProductModels/cusProductModels.js";
import { FeatureType } from "../../models/featureModels/featureEnums.js";
import { AllowanceType } from "../../models/productModels/entModels/entModels.js";
import { entIntervalToValue } from "../intervalUtils.js";
export const sortCusEntsForDeduction = (
cusEnts: (FullCustomerEntitlement & {
customer_product?: FullCusProduct;
})[],
reverseOrder: boolean = false
) => {
cusEnts.sort((a, b) => {
const aEnt = a.entitlement;
const bEnt = b.entitlement;
// 1. If boolean, go first
if (aEnt.feature.type == FeatureType.Boolean) {
return -1;
}
if (bEnt.feature.type == FeatureType.Boolean) {
return 1;
}
// 1. If a is credit system and b is not, a should go last
if (
aEnt.feature.type == FeatureType.CreditSystem &&
bEnt.feature.type != FeatureType.CreditSystem
) {
return 1;
}
// 2. If a is not credit system and b is, a should go first
if (
aEnt.feature.type != FeatureType.CreditSystem &&
bEnt.feature.type == FeatureType.CreditSystem
) {
return -1;
}
// 2. Sort by unlimited (unlimited goes first)
if (
aEnt.allowance_type == AllowanceType.Unlimited &&
bEnt.allowance_type != AllowanceType.Unlimited
) {
return -1;
}
if (
aEnt.allowance_type != AllowanceType.Unlimited &&
bEnt.allowance_type == AllowanceType.Unlimited
) {
return 1;
}
// If one has usage_allowed, it should go last
if (!a.usage_allowed && b.usage_allowed) {
return -1;
}
if (!b.usage_allowed && a.usage_allowed) {
return 1;
}
// If one has a next_reset_at, it should go first
let nextResetFirst = reverseOrder ? 1 : -1;
if (a.next_reset_at && !b.next_reset_at) {
return nextResetFirst;
}
// If b has a next_reset_at, it should go first
if (!a.next_reset_at && b.next_reset_at) {
return -nextResetFirst;
}
// 3. Sort by interval
let aVal = entIntervalToValue(aEnt.interval, aEnt.interval_count);
let bVal = entIntervalToValue(bEnt.interval, bEnt.interval_count);
if (aEnt.interval && bEnt.interval && !aVal.eq(bVal)) {
if (reverseOrder) {
return bVal.sub(aVal).toNumber();
// return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval];
} else {
return aVal.sub(bVal).toNumber();
// return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval];
}
}
// Check if a is main product
let aIsAddOn = a.customer_product?.product?.is_add_on;
let bIsAddOn = b.customer_product?.product?.is_add_on;
if (aIsAddOn && !bIsAddOn) {
return 1;
}
if (!aIsAddOn && bIsAddOn) {
return -1;
}
// 4. Sort by created_at
return a.created_at - b.created_at;
});
// console.log(
// `Cus ents after (${reverseOrder ? "reversed" : "normal"})`,
// cusEnts.map(
// (ce) => `${ce.entitlement.feature_id} - ${ce.entitlement.interval}`
// )
// );
};

View File

@@ -0,0 +1,17 @@
import { FullCusProduct } from "../../models/cusProductModels/cusProductModels.js";
export const isCanceled = ({ cusProduct }: { cusProduct: FullCusProduct }) => {
return cusProduct.canceled;
};
export const isTrialing = ({
cusProduct,
now,
}: {
cusProduct: FullCusProduct;
now?: number;
}) => {
return (
cusProduct.trial_ends_at && cusProduct.trial_ends_at > (now || Date.now())
);
};

View File

@@ -0,0 +1,123 @@
import {
BillingType,
CusProductStatus,
FullCusEntWithFullCusProduct,
FullCusProduct,
FullCustomerEntitlement,
FullCustomerPrice,
FullProduct,
} from "@autumn/shared";
import { getBillingType } from "../productUtils/priceUtils.js";
import { sortCusEntsForDeduction } from "../cusEntUtils/sortCusEntsForDeduction.js";
export const cusProductsToPrices = ({
cusProducts,
}: {
cusProducts: FullCusProduct[];
}) => {
return cusProducts.flatMap((cp) => cusProductToPrices({ cusProduct: cp }));
};
export const cusProductsToCusPrices = ({
cusProducts,
inStatuses,
billingType,
}: {
cusProducts: FullCusProduct[];
inStatuses?: CusProductStatus[];
billingType?: BillingType;
}) => {
const cusPrices: FullCustomerPrice[] = [];
for (const cusProduct of cusProducts) {
if (inStatuses && !inStatuses.includes(cusProduct.status)) {
continue;
}
let prices = cusProduct.customer_prices;
if (billingType) {
prices = prices.filter(
(cp) => getBillingType(cp.price.config) === billingType
);
}
cusPrices.push(...prices);
}
return cusPrices;
};
export const cusProductsToCusEnts = ({
cusProducts,
inStatuses = [CusProductStatus.Active],
reverseOrder = false,
featureId,
}: {
cusProducts: FullCusProduct[];
inStatuses?: CusProductStatus[];
reverseOrder?: boolean;
featureId?: string;
}) => {
let cusEnts: FullCustomerEntitlement[] = [];
for (const cusProduct of cusProducts) {
if (!inStatuses.includes(cusProduct.status)) {
continue;
}
cusEnts.push(
...cusProduct.customer_entitlements.map((cusEnt) => ({
...cusEnt,
customer_product: cusProduct,
}))
);
}
if (featureId) {
cusEnts = cusEnts.filter(
(cusEnt) => cusEnt.entitlement.feature_id === featureId
);
}
sortCusEntsForDeduction(cusEnts, reverseOrder);
return cusEnts as FullCusEntWithFullCusProduct[];
};
export const cusProductToPrices = ({
cusProduct,
billingType,
}: {
cusProduct: FullCusProduct;
billingType?: BillingType;
}) => {
let prices = cusProduct.customer_prices.map((cp) => cp.price);
if (billingType) {
prices = prices.filter((p) => getBillingType(p.config) === billingType);
}
return prices;
};
export const cusProductToEnts = ({
cusProduct,
}: {
cusProduct: FullCusProduct;
}) => {
return cusProduct.customer_entitlements.map((ce) => ce.entitlement);
};
export const cusProductToProduct = ({
cusProduct,
}: {
cusProduct: FullCusProduct;
}) => {
return {
...cusProduct.product,
prices: cusProductToPrices({ cusProduct }),
entitlements: cusProductToEnts({ cusProduct }),
free_trial: cusProduct.free_trial,
} as FullProduct;
};

View File

@@ -0,0 +1,12 @@
import { CusProductStatus } from "../../models/cusProductModels/cusProductEnums.js";
export const ACTIVE_STATUSES = [
CusProductStatus.Active,
CusProductStatus.PastDue,
];
export const RELEVANT_STATUSES = [
CusProductStatus.Active,
CusProductStatus.PastDue,
CusProductStatus.Scheduled,
];

View File

@@ -0,0 +1,38 @@
import { CusProductStatus, FullCusProduct } from "@autumn/shared";
import { nullish } from "../utils.js";
export const productToCusProduct = ({
productId,
cusProducts,
internalEntityId,
cusProductId,
version,
inStatuses,
}: {
productId: string;
cusProducts: FullCusProduct[];
internalEntityId?: string;
cusProductId?: string;
version?: number;
inStatuses?: CusProductStatus[];
}) => {
if (cusProductId) {
return cusProducts.find((cusProduct) => cusProduct.id === cusProductId);
}
return cusProducts.find((cusProduct) => {
let prodIdMatch = cusProduct.product.id === productId;
let entityMatch = internalEntityId
? cusProduct.internal_entity_id === internalEntityId
: nullish(cusProduct.internal_entity_id);
let versionMatch = version ? cusProduct.product.version === version : true;
let statusMatch = inStatuses
? inStatuses.includes(cusProduct.status)
: true;
return prodIdMatch && entityMatch && versionMatch && statusMatch;
});
};

9
shared/utils/index.ts Normal file
View File

@@ -0,0 +1,9 @@
// Cus ent utils
export * from "./cusEntUtils/sortCusEntsForDeduction.js";
// Cus product utils
export * from "./cusProductUtils/classifyCusProduct.js";
export * from "./cusProductUtils/convertCusProduct.js";
export * from "./cusProductUtils/productIdToCusProduct.js";
export * from "./cusProductUtils/cusProductConstants.js";
export * from "./utils.js";

View File

@@ -3,23 +3,17 @@ import {
Feature,
FeatureUsageType,
FrontendProductItem,
Infinite,
ProductItem,
ProductItemInterval,
RolloverConfig,
RolloverDuration,
} from "@autumn/shared";
import { toast } from "sonner";
import { isFeatureItem, isFeaturePriceItem } from "../getItemType";
import { isOneOffProduct } from "../priceUtils";
export const validateProductItem = ({
item,
// show,
features,
}: {
item: FrontendProductItem;
// show: any;
features: Feature[];
}) => {
const feature = features.find((f) => f.id == item.feature_id);

View File

@@ -3,9 +3,9 @@
import LoadingScreen from "@/views/general/LoadingScreen";
import { CusProductStatus } from "@autumn/shared";
import { CustomerContext } from "./CustomerContext";
import { notNullish } from "@/utils/genUtils";
import { getRedirectUrl, notNullish, pushPage } from "@/utils/genUtils";
import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "react-router";
import { Link, useParams, useSearchParams } from "react-router";
import { useCusQuery } from "./hooks/useCusQuery";
import { CustomerSidebar } from "./components/customer-sidebar/CustomerSidebar";
import { CustomerPageHeader } from "./components/customer-header/CustomerPageHeader";
@@ -15,6 +15,7 @@ import { CustomerEntitlementsList } from "./entitlements/CustomerEntitlementsLis
import { useCusReferralQuery } from "./hooks/useCusReferralQuery";
import { InvoicesTable } from "./InvoicesTable";
import { CustomerEventsList } from "./CustomerEventsList";
import ErrorScreen from "@/views/general/ErrorScreen";
export default function CustomerView() {
// const { customer_id } = useParams();
@@ -56,19 +57,19 @@ export default function CustomerView() {
if (cusLoading) return <LoadingScreen />;
// if (!data) {
// return (
// <ErrorScreen>
// <div className="text-t2 text-sm">Customer not found</div>
// <Link
// className="text-t3 text-xs hover:underline"
// to={getRedirectUrl("/customers", env)}
// >
// Return
// </Link>
// </ErrorScreen>
// );
// }
if (!customer) {
return (
<ErrorScreen>
<div className="text-t2 text-sm">Customer not found</div>
<Link
className="text-t3 text-xs hover:underline"
to={pushPage({ path: "/customers" })}
>
Return
</Link>
</ErrorScreen>
);
}
// const { customer, products, invoices, coupons, discount, events, entities } =
// data;
@@ -106,9 +107,9 @@ export default function CustomerView() {
<div className="flex flex-col gap-2">
<CustomerProductList />
</div>
<CustomerEntitlementsList />
<div className="flex flex-col gap-2"></div>
<div className="flex flex-col gap-2">
<CustomerEntitlementsList />
</div>
<InvoicesTable />
<CustomerEventsList />

View File

@@ -1,6 +1,5 @@
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router";
@@ -15,8 +14,12 @@ export const useCusQuery = () => {
const cachedCustomer = useMemo(getCachedCustomer, [getCachedCustomer]);
const fetcher = async () => {
const { data } = await axiosInstance.get(`/customers/${customer_id}`);
return data;
try {
const { data } = await axiosInstance.get(`/customers/${customer_id}`);
return data;
} catch (error) {
return null;
}
};
const {

View File

@@ -3,7 +3,13 @@
import ProductSidebar from "@/views/products/product/ProductSidebar";
import LoadingScreen from "@/views/general/LoadingScreen";
import { useState, useEffect, useRef } from "react";
import { Customer, Entity, Feature, ProductItem } from "@autumn/shared";
import {
Customer,
Entity,
Feature,
ProductItem,
ProductV2,
} from "@autumn/shared";
import { useAxiosSWR } from "@/services/useAxiosSwr";
import { CustomToaster } from "@/components/general/CustomToaster";
import { ManageProduct } from "@/views/products/product/ManageProduct";
@@ -15,6 +21,11 @@ import { useEnv } from "@/utils/envUtils";
import { CustomerProductBreadcrumbs } from "./components/CustomerProductBreadcrumbs";
import { FrontendProduct, useAttachState } from "./hooks/useAttachState";
import { sortProductItems } from "@/utils/productUtils";
import { useCusQuery } from "../hooks/useCusQuery";
import { useOrg } from "@/hooks/common/useOrg";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
import { useCusProductQuery } from "./hooks/useCusProductQuery";
import { notNullish } from "@/utils/genUtils";
interface OptionValue {
feature_id: string;
@@ -45,15 +56,46 @@ export default function CustomerProductView() {
const [searchParams] = useSearchParams();
const entityIdParam = searchParams.get("entity_id");
const env = useEnv();
const { isLoading: orgLoading } = useOrg();
const { isLoading: featuresLoading } = useFeaturesQuery();
const env = useEnv();
const initialProductRef = useRef<ProductV2 | null>(null);
const [product, setProduct] = useState<ProductV2 | null>(null);
const initialProductRef = useRef<FrontendProduct | null>(null);
const [product, setProduct] = useState<FrontendProduct | null>(null);
const [features, setFeatures] = useState<Feature[]>([]);
const [options, setOptions] = useState<OptionValue[]>([]);
const [entityId, setEntityId] = useState<string | null>(entityIdParam);
const [entityFeatureIds, setEntityFeatureIds] = useState<string[]>([]);
const version = searchParams.get("version");
const customer_product_id = searchParams.get("id");
// const { data, isLoading, error } = useAxiosSWR({
// url: `/customers/${customer_id}/product/${product_id}${getProductUrlParams({
// version,
// customer_product_id,
// entity_id: entityId,
// })}`,
// env,
// });
const {
product: originalProduct,
cusProduct,
isLoading,
error,
} = useCusProductQuery();
const { isLoading: cusLoading } = useCusQuery();
const attachState = useAttachState({
product,
setProduct,
initialProductRef,
cusProduct,
});
useEffect(() => {
if (entityIdParam) {
setEntityId(entityIdParam);
@@ -62,30 +104,11 @@ export default function CustomerProductView() {
}
}, [entityIdParam]);
const version = searchParams.get("version");
const customer_product_id = searchParams.get("id");
const { data, isLoading, mutate, error } = useAxiosSWR({
url: `/customers/${customer_id}/product/${product_id}${getProductUrlParams({
version,
customer_product_id,
entity_id: entityId,
})}`,
env,
});
const [selectedEntitlementAllowance, setSelectedEntitlementAllowance] =
useState<"unlimited" | number>(0);
const attachState = useAttachState({
product,
setProduct,
initialProductRef,
});
useEffect(() => {
if (!data?.product || !data?.customer) return;
if (!originalProduct) return;
const product = originalProduct;
const product = data.product;
setProduct(product);
initialProductRef.current = structuredClone({
...product,
@@ -96,22 +119,18 @@ export default function CustomerProductView() {
Array.from(
new Set(
product.items
.filter((item: ProductItem) => item.entity_feature_id != null)
.map((item: ProductItem) => item.entity_feature_id)
.filter((item: ProductItem) => notNullish(item.entity_feature_id))
.map((item: ProductItem) => item.entity_feature_id!)
)
)
);
if (product.options) {
setOptions(product.options);
if (cusProduct?.options) {
setOptions(cusProduct.options);
} else {
setOptions([]);
}
if (data?.features) {
setFeatures(data.features);
}
}, [data]);
}, [originalProduct, cusProduct]);
if (error) {
return (
@@ -123,28 +142,33 @@ export default function CustomerProductView() {
);
}
if (isLoading || !product) return <LoadingScreen />;
// console.log("Product:", product);
// console.log("Is loading:", isLoading);
// console.log("Is org loading:", orgLoading);
// console.log("Is cus loading:", cusLoading);
// console.log("Is features loading:", featuresLoading);
if (isLoading || !product || cusLoading || orgLoading || featuresLoading)
return <LoadingScreen />;
if (!customer_id || !product_id) {
return <div>Customer or product not found</div>;
}
const { customer } = data;
return (
<ProductContext.Provider
value={{
...data,
features,
setFeatures,
mutate,
env,
// ...data,
// features,
// setFeatures,
// mutate,
// env,
isCusProductView: true,
product,
setProduct,
selectedEntitlementAllowance,
setSelectedEntitlementAllowance,
customer: customer as Customer,
entities: data.entities as Entity[],
entityId,
setEntityId,
attachState,
@@ -200,3 +224,17 @@ export const CopyUrl = ({
</div>
);
};
export type NewSubscription = {
id: "new";
code: "checkout_session" | "combine_subscription" | "renew";
};
export type UpdateSubscription = {
id: "update";
code: "upgrade" | "downgrade";
};
export type Product = {
scenario: "new:checkout_session" | "new:combine_subscription";
};

View File

@@ -8,20 +8,23 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
import { getBackendErr } from "@/utils/genUtils";
import { toast } from "sonner";
import { getAttachBody } from "./attachProductUtils";
import { useCusQuery } from "../../hooks/useCusQuery";
import { useProductQueryState } from "@/views/products/product/hooks/useProductQuery";
export const AttachButton = () => {
const axios = useAxiosInstance();
const [open, setOpen] = useState(false);
const [buttonLoading, setButtonLoading] = useState(false);
const { attachState, product, entityId, customer, version } =
useProductContext();
const { preview, setPreview } = attachState;
const { customer } = useCusQuery();
const { buttonText } = attachState;
const { queryStates } = useProductQueryState();
const { attachState, product, entityId } = useProductContext();
const { buttonText, setPreview } = attachState;
const handleAttachClicked = async () => {
setButtonLoading(true);
try {
const res = await axios.post(
"/v1/attach/preview",
@@ -30,15 +33,17 @@ export const AttachButton = () => {
attachState,
product,
entityId,
version: version || product.version,
version: queryStates.version || product.version,
})
);
setPreview(res.data);
setOpen(true);
} catch (error) {
console.log("error", error);
toast.error(getBackendErr(error, "Failed to attach product"));
}
setButtonLoading(false);
};

View File

@@ -1,3 +1,5 @@
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import {
@@ -26,12 +28,11 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useEnv } from "@/utils/envUtils";
import { getStripeInvoiceLink } from "@/utils/linkUtils";
import { AttachPreviewDetails } from "./AttachPreviewDetails";
import { ToggleConfigButton } from "./ToggleConfigButton";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { AttachInfo } from "./attach-preview/AttachInfo";
import { getAttachBody } from "./attachProductUtils";
import { InvoiceCustomerButton } from "./InvoiceCustomerButton";
import { useCusQuery } from "../../hooks/useCusQuery";
export const AttachModal = ({
open,
@@ -40,16 +41,16 @@ export const AttachModal = ({
open: boolean;
setOpen: (open: boolean) => void;
}) => {
const { product, customer, entities, entityId, attachState, version } =
useProductContext();
const { customer, entities } = useCusQuery();
const { product, entityId, attachState, version } = useProductContext();
const navigation = useNavigate();
const env = useEnv();
const axiosInstance = useAxiosInstance();
const { preview, options, setOptions, flags } = attachState;
const { preview, options, flags } = attachState;
const [checkoutLoading, setCheckoutLoading] = useState(false);
const [invoiceLoading, setInvoiceLoading] = useState(false);
const [invoiceLoading] = useState(false);
const getName = () => {
const cusName =
@@ -191,9 +192,10 @@ export const AttachModal = ({
}
};
const [configOpen, setConfigOpen] = useState(false);
const [configOpen] = useState(false);
const mainWidth = "w-lg";
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="translate-y-[0%] top-[20%] max-h-[70vh] duration-0 p-0 overflow-y-auto">
@@ -210,10 +212,12 @@ export const AttachModal = ({
<div className="text-sm flex flex-col gap-4">
<div className="flex flex-col">
<p className="text-t2 font-semibold mb-2">Details</p>
<PriceItem>
<span>Product</span>
<span>{product?.name}</span>
</PriceItem>
<PriceItem>
<span>Customer</span>
<span>{getName()}</span>
@@ -221,6 +225,7 @@ export const AttachModal = ({
</div>
{preview && !flags.isFree && <AttachPreviewDetails />}
<AttachInfo />
</div>

View File

@@ -8,7 +8,7 @@ import { AttachBranch } from "@autumn/shared";
import { OptionsInput } from "./attach-preview/OptionsInput";
export const AttachPreviewDetails = () => {
const { org, attachState } = useProductContext();
const { attachState } = useProductContext();
const { preview } = attachState;
if (!preview) {
@@ -20,9 +20,6 @@ export const AttachPreviewDetails = () => {
return (
<React.Fragment>
{/* Options */}
{/* <OptionsInput /> */}
<UpdateQuantity />
{!isUpdatePrepaidQuantity && (
<>

View File

@@ -1,8 +1,10 @@
import { Badge } from "@/components/ui/badge";
import { useProductContext } from "@/views/products/product/ProductContext";
import { useCusQuery } from "../../hooks/useCusQuery";
export const CustomerProductBadge = () => {
const { product, customer } = useProductContext();
const { customer } = useCusQuery();
const { product } = useProductContext();
if (!customer) return null;

View File

@@ -6,13 +6,14 @@ import {
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { useEnv } from "@/utils/envUtils";
import { navigateTo } from "@/utils/genUtils";
import { navigateTo, pushPage } from "@/utils/genUtils";
import { useProductContext } from "@/views/products/product/ProductContext";
import { useNavigate } from "react-router";
import { useCusQuery } from "../../hooks/useCusQuery";
export const CustomerProductBreadcrumbs = () => {
const env = useEnv();
const { customer, product, entityId } = useProductContext();
const { customer } = useCusQuery();
const { product, entityId } = useProductContext();
const navigation = useNavigate();
return (
@@ -21,7 +22,7 @@ export const CustomerProductBreadcrumbs = () => {
<BreadcrumbItem>
<BreadcrumbLink
className="cursor-pointer"
onClick={() => navigateTo("/customers", navigation, env)}
onClick={() => navigateTo("/customers", navigation)}
>
Customers
</BreadcrumbLink>
@@ -30,13 +31,13 @@ export const CustomerProductBreadcrumbs = () => {
<BreadcrumbLink
className="cursor-pointer truncate max-w-48"
onClick={() =>
navigateTo(
`/customers/${customer.id || customer.internal_id}${
entityId ? `?entity_id=${entityId}` : ""
}`,
navigation,
env,
)
pushPage({
path: `/customers/${customer.id || customer.internal_id}`,
navigate: navigation,
queryParams: {
entity_id: entityId,
},
})
}
>
{customer.name

View File

@@ -1,4 +1,5 @@
import { PriceItem } from "@/components/pricing/attach-pricing-dialog";
import { useOrg } from "@/hooks/common/useOrg";
import { isFeaturePriceItem } from "@/utils/product/getItemType";
import {
getFeatureString,
@@ -7,7 +8,8 @@ import {
import { useProductContext } from "@/views/products/product/ProductContext";
export const AttachNewItems = () => {
const { attachState, features, org } = useProductContext();
const { org } = useOrg();
const { attachState, features } = useProductContext();
const { preview } = attachState;
if (preview?.new_items) {

View File

@@ -6,9 +6,14 @@ import { useProductContext } from "@/views/products/product/ProductContext";
import { getFeatureInvoiceDescription } from "@autumn/shared";
import { formatAmount } from "@/utils/formatUtils/formatTextUtils";
import { AdjustableOptions } from "./AdjustQuantity";
import { useOrg } from "@/hooks/common/useOrg";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
export const DueNextCycle = () => {
const { attachState, product, features, org } = useProductContext();
const { org } = useOrg();
const { features } = useFeaturesQuery();
const { attachState, product } = useProductContext();
const preview = attachState.preview;
const currency = org.default_currency || "USD";

View File

@@ -13,9 +13,11 @@ import {
import { Decimal } from "decimal.js";
import { Input } from "@/components/ui/input";
import { notNullish } from "@/utils/genUtils";
import { useOrg } from "@/hooks/common/useOrg";
export const DueToday = () => {
const { attachState, product, org } = useProductContext();
const { org } = useOrg();
const { attachState, product } = useProductContext();
const { preview, options, setOptions } = attachState;
const dueToday = preview.due_today;

View File

@@ -1,12 +1,14 @@
import { PriceItem } from "@/components/pricing/attach-pricing-dialog";
import { Input } from "@/components/ui/input";
import { useOrg } from "@/hooks/common/useOrg";
import { notNullish } from "@/utils/genUtils";
import { formatAmount } from "@/utils/product/productItemUtils";
import { useProductContext } from "@/views/products/product/ProductContext";
import { AttachBranch } from "@autumn/shared";
export const UpdateQuantity = () => {
const { attachState, product, org } = useProductContext();
const { org } = useOrg();
const { attachState, product } = useProductContext();
const { preview, options, setOptions } = attachState;
if (preview.branch !== AttachBranch.UpdatePrepaidQuantity) {

View File

@@ -4,7 +4,10 @@ import { isOneOffProduct } from "@/utils/product/priceUtils";
import { sortProductItems } from "@/utils/productUtils";
import {
AttachPreview,
CusProduct,
FeatureOptions,
FullCusProduct,
isCanceled,
ProductItem,
ProductV2,
UsageModel,
@@ -32,22 +35,25 @@ const productHasPrepaid = (items: ProductItem[]) => {
);
};
const productIsAddOn = (product: FrontendProduct) => {
const productIsAddOn = (product: ProductV2) => {
return product.is_add_on;
};
const productIsFree = (product: FrontendProduct) => {
const productIsFree = (product: ProductV2) => {
return product.items.every((item) => isFeatureItem(item));
};
export const useAttachState = ({
product,
cusProduct,
setProduct,
initialProductRef,
}: {
product: FrontendProduct | null;
setProduct: (product: FrontendProduct) => void;
initialProductRef: React.RefObject<FrontendProduct | null>;
product: ProductV2 | null;
setProduct: (product: ProductV2) => void;
cusProduct: FullCusProduct | undefined;
initialProductRef: React.RefObject<ProductV2>;
}) => {
const [preview, setPreview] = useState<AttachPreview | null>(null);
const [options, setOptions] = useState<
@@ -58,12 +64,13 @@ export const useAttachState = ({
>([]);
const [itemsChanged, setItemsChanged] = useState(false);
const [flags, setFlags] = useState({
hasPrepaid: product ? productHasPrepaid(product.items) : false,
isAddOn: product ? productIsAddOn(product) : false,
isFree: product ? productIsFree(product) : false,
isCanceled: product ? product.isCanceled : false,
isCanceled: cusProduct ? isCanceled({ cusProduct }) : false,
isOneOff: product
? isOneOffProduct(product.items, product.is_add_on)
: false,
@@ -80,7 +87,7 @@ export const useAttachState = ({
hasPrepaid: product ? productHasPrepaid(product.items) : false,
isAddOn: product ? productIsAddOn(product) : false,
isFree: product ? productIsFree(product) : false,
isCanceled: product ? product.isCanceled : false,
isCanceled: cusProduct ? isCanceled({ cusProduct }) : false,
isOneOff: product
? isOneOffProduct(product.items, product.is_add_on)
: false,
@@ -119,7 +126,7 @@ export const useAttachState = ({
const getButtonDisabled = () => {
if (flags.isOneOff) return false;
if (product?.isActive && !itemsChanged && !flags.isCanceled) {
if (cusProduct && !itemsChanged && !flags.isCanceled) {
if (flags.hasPrepaid) {
return false;
}
@@ -147,7 +154,7 @@ export const useAttachState = ({
return AttachCase.OneOff;
}
if (product?.isActive && !itemsChanged) {
if (cusProduct && !itemsChanged) {
return AttachCase.Active;
}
@@ -157,7 +164,7 @@ export const useAttachState = ({
};
const getButtonText = () => {
if (product?.isActive && !itemsChanged) {
if (cusProduct && !itemsChanged) {
if (flags.isOneOff) {
return "Attach Product";
}

View File

@@ -0,0 +1,61 @@
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { FullCusProduct, ProductV2 } from "@autumn/shared";
import { useQuery } from "@tanstack/react-query";
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
import { useParams } from "react-router";
export const useCusProductQuery = () => {
const axiosInstance = useAxiosInstance();
const { customer_id, product_id } = useParams();
const [queryStates] = useQueryStates({
version: parseAsInteger,
customer_product_id: parseAsString,
entity_id: parseAsString,
});
const fetcher = async () => {
const queryParams = {
version: queryStates.version,
customer_product_id: queryStates.customer_product_id,
entity_id: queryStates.entity_id,
};
try {
const { data } = await axiosInstance.get(
`/customers/${customer_id}/product/${product_id}`,
{ params: queryParams }
);
return data;
} catch (error) {
return null;
}
};
const { data, isLoading, error, refetch } = useQuery<{
cusProduct: FullCusProduct;
product: ProductV2;
}>({
queryKey: ["customer_product", customer_id, product_id],
queryFn: fetcher,
});
return {
cusProduct: data?.cusProduct,
product: data?.product,
isLoading,
error,
refetch,
};
// const { data, isLoading, error, refetch } = useQuery({
// queryKey: ["customer_product", customer_id, product_id],
// queryFn: fetcher,
// });
// return {
// cusProduct,
// isLoading,
// error,
// refetch,
// };
};

View File

@@ -77,10 +77,10 @@ export const CreateFeature = ({
}
);
await refetch();
if (onSuccess) {
await onSuccess(createdFeature);
} else {
await refetch();
setOpen(false);
}
} catch (error) {

View File

@@ -15,7 +15,7 @@ import { UpdateProductButton } from "./components/UpdateProductButton";
import { isOneOffProduct } from "@/utils/product/priceUtils";
export default function ProductSidebar() {
const { product, setProduct, customer } = useProductContext();
const { product, setProduct, isCusProductView } = useProductContext();
const [freeTrialModalOpen, setFreeTrialModalOpen] = useState(false);
const [entitiesOpen, setEntitiesOpen] = useState(false);
const [accordionValues, setAccordionValues] = useState([
@@ -32,8 +32,6 @@ export default function ProductSidebar() {
setProduct({ ...product, free_trial: null });
};
const isCustomerProductView = notNullish(customer);
const handleAccordionToggle = (value: string) => {
setAccordionValues((prev) =>
prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]
@@ -43,7 +41,7 @@ export default function ProductSidebar() {
return (
<div className="flex-col gap-4 h-full border-l py-6">
<div className="flex items-center gap-2 justify-start px-4">
{isCustomerProductView && <CustomerProductBadge />}
{isCusProductView && <CustomerProductBadge />}
</div>
<Accordion
@@ -57,11 +55,11 @@ export default function ProductSidebar() {
<ProductProps />
</SideAccordion>
</div>
<div className="flex w-full border-b p-4">
{/* <div className="flex w-full border-b p-4">
<SideAccordion title="Versions" value="versions">
<ProductVersions />
</SideAccordion>
</div>
</div> */}
<div className="flex w-full border-b p-4 relative">
<SideAccordion
title="Entities"
@@ -110,7 +108,7 @@ export default function ProductSidebar() {
</SideAccordion>
</div>
<div className="flex gap-2 px-4 py-6 w-full">
{isCustomerProductView ? <AttachButton /> : <UpdateProductButton />}
{isCusProductView ? <AttachButton /> : <UpdateProductButton />}
</div>
</Accordion>
</div>

View File

@@ -19,6 +19,7 @@ import { getBackendErr } from "@/utils/genUtils";
import { useProductChangedAlert } from "./hooks/useProductChangedAlert";
import { useProductData } from "./hooks/useProductData";
import { UpdateProductButton } from "./components/UpdateProductButton";
import { useProductQuery } from "./hooks/useProductQuery";
function ProductView({ env }: { env: AppEnv }) {
const axiosInstance = useAxiosInstance();
@@ -26,115 +27,108 @@ function ProductView({ env }: { env: AppEnv }) {
const [searchParams] = useSearchParams();
const version = searchParams.get("version");
// const url = `/products/${product_id}/data?version=${version}`;
// const { data, isLoading, mutate } = useAxiosSWR({ url });
// const countUrl = `/products/${product_id}/count?version=${version}`;
// const { data: counts, mutate: mutateCount } = useAxiosSWR({ url: countUrl });
const [showNewVersionDialog, setShowNewVersionDialog] = useState(false);
const url = `/products/${product_id}/data?version=${version}`;
const { data, isLoading, mutate } = useAxiosSWR({ url });
const countUrl = `/products/${product_id}/count?version=${version}`;
const { data: counts, mutate: mutateCount } = useAxiosSWR({ url: countUrl });
//this is to make sure pricing for unlimited entitlements can't be applied
const [selectedEntitlementAllowance, setSelectedEntitlementAllowance] =
useState<"unlimited" | number>(0);
const { product: originalProduct, isLoading, error } = useProductQuery();
const {
product,
setProduct,
hasChanges,
features,
setFeatures,
entityFeatureIds,
setEntityFeatureIds,
actionState,
isNewProduct,
} = useProductData({
originalProduct: data?.product as any,
originalFeatures: data?.features as any,
});
} = useProductData({ originalProduct });
const { modal } = useProductChangedAlert({ hasChanges });
const [buttonLoading, setButtonLoading] = useState(false);
if (isLoading) return <LoadingScreen />;
if (!product) {
if (error) {
return (
<ErrorScreen returnUrl="/products">
Product {product_id} not found
{error ? error.message : `Product ${product_id} not found`}
</ErrorScreen>
);
}
const updateProduct = async () => {
try {
await ProductService.updateProduct(axiosInstance, product.id, {
...UpdateProductSchema.parse(product),
items: product.items,
free_trial: product.free_trial,
});
if (!product) return <></>;
if (isNewProduct) {
toast.success("Product created successfully");
} else {
toast.success("Product updated successfully");
}
// const updateProduct = async () => {
// try {
// await ProductService.updateProduct(axiosInstance, product.id, {
// ...UpdateProductSchema.parse(product),
// items: product.items,
// free_trial: product.free_trial,
// });
await mutate();
await mutateCount();
} catch (error) {
toast.error(getBackendErr(error, "Failed to update product"));
}
};
// if (isNewProduct) {
// toast.success("Product created successfully");
// } else {
// toast.success("Product updated successfully");
// }
const updateProductClicked = async () => {
if (!counts) {
toast.error("Something went wrong, please try again...");
return;
}
// await refetch();
// await mutateCount();
// } catch (error) {
// toast.error(getBackendErr(error, "Failed to update product"));
// }
// };
if (version && version < data?.numVersions) {
toast.error("You can only update the latest version of a product");
return;
}
// const updateProductClicked = async () => {
// if (!counts) {
// toast.error("Something went wrong, please try again...");
// return;
// }
if (counts?.all > 0) {
setShowNewVersionDialog(true);
return;
}
// if (version && version < data?.numVersions) {
// toast.error("You can only update the latest version of a product");
// return;
// }
await updateProduct();
};
// if (counts?.all > 0) {
// setShowNewVersionDialog(true);
// return;
// }
// await updateProduct();
// };
return (
<ProductContext.Provider
value={{
...data,
features,
setFeatures,
mutate,
env,
// ...data,
// features,
// setFeatures,
// For versioning?
// counts,
// version,
// mutate,
// env,
product,
setProduct,
selectedEntitlementAllowance,
setSelectedEntitlementAllowance,
counts,
version,
mutateCount,
// mutateCount,
actionState,
handleCreateProduct: updateProductClicked,
// handleCreateProduct: updateProductClicked,
entityFeatureIds,
setEntityFeatureIds,
hasChanges,
buttonLoading,
setButtonLoading,
}}
>
<ConfirmNewVersionDialog
{/* <ConfirmNewVersionDialog
open={showNewVersionDialog}
setOpen={setShowNewVersionDialog}
createProduct={updateProduct}
/>
/> */}
<div className="flex w-full">
<div className="flex flex-col gap-4 w-full">
<ProductViewBreadcrumbs />

View File

@@ -11,9 +11,9 @@ import { useProductContext } from "../ProductContext";
import { useEnv } from "@/utils/envUtils";
export default function ProductViewBreadcrumbs() {
let { product } = useProductContext();
let env = useEnv();
let navigate = useNavigate();
const { product } = useProductContext();
const env = useEnv();
const navigate = useNavigate();
return (
<Breadcrumb className="text-t3 pt-6 pl-10 flex justify-center">

View File

@@ -2,19 +2,51 @@ import { Button } from "@/components/ui/button";
import { useState } from "react";
import { useProductContext } from "@/views/products/product/ProductContext";
import { Upload } from "lucide-react";
import { useProductQuery } from "../hooks/useProductQuery";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { UpdateProductSchema } from "@autumn/shared";
import { toast } from "sonner";
import { getBackendErr } from "@/utils/genUtils";
export const UpdateProductButton = () => {
const [open, setOpen] = useState(false);
// const [loading, setLoading] = useState(false);
const [buttonLoading, setButtonLoading] = useState(false);
const axiosInstance = useAxiosInstance();
const { handleCreateProduct, actionState, buttonLoading, setButtonLoading } =
useProductContext();
const { actionState, product } = useProductContext();
const { refetch } = useProductQuery();
const updateProduct = async () => {
try {
await axiosInstance.post(`/v1/products/${product.id}`, {
...UpdateProductSchema.parse(product),
items: product.items,
free_trial: product.free_trial,
});
// await ProductService.updateProduct(axiosInstance, product.id, {
// ...UpdateProductSchema.parse(product),
// items: product.items,
// free_trial: product.free_trial,
// });
toast.success("Product updated successfully");
// if (isNewProduct) {
// toast.success("Product created successfully");
// } else {
// }
await refetch();
// await mutateCount();
} catch (error) {
toast.error(getBackendErr(error, "Failed to update product"));
}
};
return (
<Button
onClick={async () => {
setButtonLoading(true);
await handleCreateProduct(false);
await updateProduct();
setButtonLoading(false);
}}
variant="gradientPrimary"

View File

@@ -0,0 +1,53 @@
import { useQueryClient } from "@tanstack/react-query";
import { ProductV2 } from "@autumn/shared";
import { useProductQueryState } from "./useProductQuery";
export const useCachedProduct = ({
productId,
}: {
productId: string | undefined;
}) => {
const queryClient = useQueryClient();
const { queryStates } = useProductQueryState();
const version = queryStates.version;
const getCachedProduct = (): ProductV2 | null => {
if (!productId) return null;
// Check all cached full customers queries
const queryCache = queryClient.getQueryCache();
const productsQueries = queryCache.findAll({
queryKey: ["products"],
});
// Sort by most recently updated first to get the freshest data
const sortedQueries = productsQueries.sort((a, b) => {
const aTime = a.state.dataUpdatedAt || 0;
const bTime = b.state.dataUpdatedAt || 0;
return bTime - aTime;
});
for (const query of sortedQueries) {
// Only use data that's not stale and has been successfully fetched
if (query.state.status === "success" && query.state.data) {
const cachedData = query.state.data as
| { products: ProductV2[] }
| undefined;
if (cachedData?.products) {
const cachedProduct = cachedData.products.find(
(p) => p.id === productId && (version ? p.version == version : true)
);
if (cachedProduct) {
return cachedProduct;
}
}
}
}
return null;
};
return { getCachedProduct };
};

View File

@@ -5,15 +5,12 @@ import { useEffect, useRef, useState } from "react";
export const useProductData = ({
originalProduct,
originalFeatures,
}: {
originalProduct: ProductV2 | null;
originalFeatures: Feature[] | null;
}) => {
const initialProductRef = useRef<ProductV2 | null>(null);
const [hasChanges, setHasChanges] = useState(false);
const [product, setProduct] = useState<ProductV2 | null>(null);
const [features, setFeatures] = useState<Feature[]>([]);
const [entityFeatureIds, setEntityFeatureIds] = useState<string[]>([]);
const initEntityFeatureIds = (product: ProductV2 | null) => {
@@ -38,11 +35,7 @@ export const useProductData = ({
setEntityFeatureIds(initEntityFeatureIds(sortedProduct));
setProduct(sortedProduct);
}
if (originalFeatures) {
setFeatures(originalFeatures);
}
}, [originalProduct, originalFeatures]);
}, [originalProduct]);
useEffect(() => {
if (!product) return;
@@ -96,8 +89,6 @@ export const useProductData = ({
product,
setProduct,
hasChanges,
features,
setFeatures,
entityFeatureIds,
setEntityFeatureIds,
actionState,

View File

@@ -0,0 +1,54 @@
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useQuery } from "@tanstack/react-query";
import { parseAsInteger, parseAsString } from "nuqs";
import { useQueryStates } from "nuqs";
import { useParams, useSearchParams } from "react-router";
import { useCachedProduct } from "./getCachedProduct";
import { useMemo } from "react";
// Product query state...
export const useProductQueryState = () => {
const [queryStates, setQueryStates] = useQueryStates(
{
version: parseAsInteger,
},
{
history: "replace",
}
);
return { queryStates, setQueryStates };
};
export const useProductQuery = () => {
const { product_id } = useParams();
const { queryStates } = useProductQueryState();
const axiosInstance = useAxiosInstance();
const { getCachedProduct } = useCachedProduct({ productId: product_id });
const cachedProduct = useMemo(getCachedProduct, [getCachedProduct]);
const fetcher = async () => {
const url = `/products/${product_id}/data2`;
const queryParams = {
version: queryStates.version,
};
const { data } = await axiosInstance.get(url, { params: queryParams });
return data;
};
const { data, isLoading, refetch, error } = useQuery({
queryKey: ["product", product_id, queryStates.version],
queryFn: fetcher,
});
const product = data?.product || cachedProduct;
console.log("Cached product:", cachedProduct);
console.log("Error:", error);
console.log("Is loading:", isLoading);
const isLoadingWithCache = cachedProduct ? false : isLoading;
return { product, isLoading: isLoadingWithCache, refetch, error };
};

View File

@@ -1,76 +0,0 @@
import { DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
} from "@/components/ui/dialog";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { PricingConfig, validateConfig } from "./PricingConfig";
import { CreatePriceSchema, PriceType } from "@autumn/shared";
import { useProductContext } from "../ProductContext";
export const CreatePrice = () => {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [price, setPrice] = useState<any>(null);
const { product, setProduct, selectedEntitlementAllowance } =
useProductContext();
const handleCreatePrice = async () => {
if (!price) {
return;
}
setLoading(true);
const config = validateConfig(price, product.prices);
if (!config) {
return;
}
const newPrice = CreatePriceSchema.parse({
name: price.name,
config,
});
setProduct({
...product,
prices: [...product.prices, newPrice],
});
setOpen(false);
setPrice(null);
setLoading(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="dashed"
className="w-full"
startIcon={<PlusIcon size={15} />}
>
Create Price
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create Price</DialogTitle>
<PricingConfig priceConfig={price} setPriceConfig={setPrice} />
<DialogFooter>
<Button
onClick={handleCreatePrice}
isLoading={loading}
variant="gradientPrimary"
disabled={selectedEntitlementAllowance === "unlimited"}
>
Create Price
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -26,6 +26,7 @@ import { emptyPriceItem } from "./create-product-item/defaultItemConfigs";
import { itemsHaveSameInterval } from "@/utils/product/productItemUtils";
import { toast } from "sonner";
import { getFeature } from "@/utils/product/entitlementUtils";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
export const defaultProductItem: ProductItem = {
feature_id: null,
@@ -150,15 +151,12 @@ const useMergeFeaturePriceItem = ({
};
export function CreateProductItem() {
const { features } = useFeaturesQuery();
const { product, setProduct } = useProductContext();
const [open, setOpen] = useState(false);
const [showCreateFeature, setShowCreateFeature] = useState(false);
const [item, setItem] = useState<ProductItem>(defaultProductItem);
const { features, product, setProduct, setFeatures, counts, mutate } =
useProductContext();
const [configState, setConfigState] = useState({
showPrice: false,
});
const { shouldMergeFeatureItem, mergeFeatureItem } = useMergeFeatureItem({
item,

View File

@@ -19,6 +19,7 @@ import { useSteps } from "./useSteps";
import { CreateItemStep } from "./utils/CreateItemStep";
import { cn } from "@/lib/utils";
import { defaultPriceItem } from "./create-product-item/defaultItemConfigs";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
const defaultProductItem: ProductItem = {
feature_id: null,
@@ -47,14 +48,15 @@ export function CreateProductItem2({
const [open, setOpen] = useState(false);
const [showCreateFeature, setShowCreateFeature] = useState(false);
const [item, setItem] = useState<ProductItem>(defaultProductItem);
const { features, product, setProduct } = useProductContext();
const { setFirstItemCreated } = useModelPricingContext();
const { product, setProduct } = useProductContext();
const { features } = useFeaturesQuery();
const stepState = useSteps({ initialStep: CreateItemStep.SelectItemType });
const handleCreateProductItem = async (entityFeatureId?: string) => {
const validatedItem = validateProductItem({
item: {
isPrice: false,
...item,
entity_feature_id: entityFeatureId
? entityFeatureId

View File

@@ -1,8 +1,3 @@
import {
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
import {
@@ -11,12 +6,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Feature,
features,
FeatureUsageType,
ProductItem,
} from "@autumn/shared";
import { Feature, FeatureUsageType, ProductItem } from "@autumn/shared";
import { useState } from "react";
import { useProductContext } from "../ProductContext";
import { CheckIcon, PlusIcon } from "lucide-react";
@@ -24,6 +14,7 @@ import { CreateFeature } from "@/views/products/features/components/CreateFeatur
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { CustomDialogContent } from "@/components/general/modal-components/DialogContentWrapper";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
export const EntitiesDropdown = ({
open,
@@ -50,8 +41,10 @@ export const EntitiesDropdown = ({
};
export const EntitiesDropdownContent = () => {
const { product, features, entityFeatureIds, setEntityFeatureIds } =
const { features } = useFeaturesQuery();
const { product, entityFeatureIds, setEntityFeatureIds } =
useProductContext();
const [createFeatureOpen, setCreateFeatureOpen] = useState(false);
const continuousUseFeatures = features.filter(

View File

@@ -6,43 +6,25 @@ import {
ProductItemInterval,
TierInfinite,
} from "@autumn/shared";
import { useEffect, useState } from "react";
import { useProductContext } from "@/views/products/product/ProductContext";
import { cn } from "@/lib/utils";
import { useProductItemContext } from "./ProductItemContext";
import {
getShowParams,
shouldShowProrationConfig,
} from "@/utils/product/productItemUtils";
import { cn } from "@/lib/utils";
import { useEffect, useState } from "react";
import { useProductItemContext } from "./ProductItemContext";
import { useProductContext } from "@/views/products/product/ProductContext";
import { ConfigWithFeature } from "./components/ConfigWithFeature";
import FixedPriceConfig from "./components/ConfigFixedPrice";
import {
isFeatureItem,
isFeaturePriceItem,
isPriceItem,
} from "@/utils/product/getItemType";
import { PriceItemConfig } from "./product-item-config/PriceItemConfig";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
export const ProductItemConfig = () => {
// HOOKS
const { features } = useProductContext();
const { item, setItem } = useProductItemContext();
const [show, setShow] = useState(getShowParams(item));
const handleAddPrice = () => {
setItem({
...item,
tiers: [
{
to: TierInfinite,
amount: item.price ?? 0,
},
],
interval: ProductItemInterval.Month,
});
setShow({ ...show, price: !show.price });
};
const { features } = useFeaturesQuery();
const { item, setItem } = useProductItemContext();
useEffect(() => {
const feature = features.find((f: Feature) => f.id == item.feature_id);

View File

@@ -66,15 +66,15 @@ export const ProductItemRow = ({
const getFreeFeatureString = (item: ProductItem) => {
const feature = features.find((f: Feature) => f.id == item.feature_id);
if (feature?.type === FeatureType.Boolean) {
return `${feature.name}`;
}
const featureName = getName({
featureId: item.feature_id!,
units: item.included_usage,
});
if (feature?.type === FeatureType.Boolean) {
return `${featureName}`;
}
if (item.included_usage == Infinite) {
return `Unlimited ${featureName}`;
}

Some files were not shown because too many files have changed in this diff Show More