moving cusproduct to tanstack
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
cusProductToPrices,
|
||||
cusProductToEnts,
|
||||
cusProductToProduct,
|
||||
} from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
} from "@autumn/shared";
|
||||
import {
|
||||
formatPrice,
|
||||
getPriceEntitlement,
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
113
shared/utils/cusEntUtils/sortCusEntsForDeduction.ts
Normal file
113
shared/utils/cusEntUtils/sortCusEntsForDeduction.ts
Normal 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}`
|
||||
// )
|
||||
// );
|
||||
};
|
||||
17
shared/utils/cusProductUtils/classifyCusProduct.ts
Normal file
17
shared/utils/cusProductUtils/classifyCusProduct.ts
Normal 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())
|
||||
);
|
||||
};
|
||||
123
shared/utils/cusProductUtils/convertCusProduct.ts
Normal file
123
shared/utils/cusProductUtils/convertCusProduct.ts
Normal 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;
|
||||
};
|
||||
12
shared/utils/cusProductUtils/cusProductConstants.ts
Normal file
12
shared/utils/cusProductUtils/cusProductConstants.ts
Normal 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,
|
||||
];
|
||||
38
shared/utils/cusProductUtils/productIdToCusProduct.ts
Normal file
38
shared/utils/cusProductUtils/productIdToCusProduct.ts
Normal 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
9
shared/utils/index.ts
Normal 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";
|
||||
@@ -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);
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
// };
|
||||
};
|
||||
@@ -77,10 +77,10 @@ export const CreateFeature = ({
|
||||
}
|
||||
);
|
||||
|
||||
await refetch();
|
||||
if (onSuccess) {
|
||||
await onSuccess(createdFeature);
|
||||
} else {
|
||||
await refetch();
|
||||
setOpen(false);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
53
vite/src/views/products/product/hooks/getCachedProduct.tsx
Normal file
53
vite/src/views/products/product/hooks/getCachedProduct.tsx
Normal 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 };
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
54
vite/src/views/products/product/hooks/useProductQuery.tsx
Normal file
54
vite/src/views/products/product/hooks/useProductQuery.tsx
Normal 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 };
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user