diff --git a/example/src/app/referrals/functions.tsx b/example/src/app/referrals/functions.tsx index 3db280e34..4312ee4b1 100644 --- a/example/src/app/referrals/functions.tsx +++ b/example/src/app/referrals/functions.tsx @@ -23,7 +23,6 @@ export const redeemReferralCode = async ({ customerId, code: referralCode, }); - console.log("Referral code redeemed"); - console.log("Redemption", redemption); + return redemption; }; diff --git a/example/src/app/referrals/page.tsx b/example/src/app/referrals/page.tsx index 7e56ff93b..108289c19 100644 --- a/example/src/app/referrals/page.tsx +++ b/example/src/app/referrals/page.tsx @@ -17,7 +17,6 @@ const useReferralCode = (referrerId: string) => { const { code } = await getReferralCode(referrerId); setReferralCode(code); } catch (error) { - console.log("Failed to get referral code", error); toast.error(`Error fetching referral code: ${error}`); } setLoading(false); diff --git a/server/src/internal/api/customers/cusRouter.ts b/server/src/internal/api/customers/cusRouter.ts index 05c6c0bc3..85e61285f 100644 --- a/server/src/internal/api/customers/cusRouter.ts +++ b/server/src/internal/api/customers/cusRouter.ts @@ -247,109 +247,6 @@ cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => { } }); -// Entitlements -// cusRouter.get("/:customer_id/entitlements", async (req: any, res: any) => { -// const customerId = req.params.customer_id; - -// let balances: any[] = []; -// const customer = await CusService.getById({ -// sb: req.sb, -// id: customerId, -// orgId: req.orgId, -// env: req.env, -// logger: req.logtail, -// }); - -// const fullCusProducts = await CusService.getFullCusProducts({ -// sb: req.sb, -// internalCustomerId: customer.internal_id, -// withPrices: true, -// logger: req.logtail, -// }); - -// const cusEntsWithCusProduct = fullCusProductToCusEnts(fullCusProducts); -// const cusPrices = fullCusProductToCusPrices(fullCusProducts); - -// balances = await getCusBalancesByEntitlement({ -// cusEntsWithCusProduct: cusEntsWithCusProduct as any, -// cusPrices, -// entities, -// }); - -// for (const balance of balances) { -// if (balance.total && balance.balance) { -// balance.used = balance.total - balance.balance; -// delete balance.total; -// } -// } - -// res.status(200).json(balances); -// }); - -// Invoice - cusRouter.post("/:customer_id/coupons/:coupon_id", handleAddCouponToCus); cusRouter.use("/:customer_id/entities", entityRouter); - -// cusRouter.put("", async (req: any, res: any) => { -// try { -// const { id, name, email, fingerprint, reset_at } = req.body; - -// if (!id && !email) { -// throw new RecaseError({ -// message: "Customer ID or email is required", -// code: ErrCode.InvalidCustomer, -// statusCode: StatusCodes.BAD_REQUEST, -// }); -// } - -// let existingCustomers = await CusService.getByIdOrEmail({ -// sb: req.sb, -// id, -// email, -// orgId: req.orgId, -// env: req.env, -// }); - -// if (existingCustomers.length > 1) { -// throw new RecaseError({ -// message: "Multiple customers found", -// code: ErrCode.MultipleCustomersFound, -// statusCode: StatusCodes.CONFLICT, -// }); -// } - -// let newCustomer: Customer; -// if (existingCustomers.length == 1) { -// const existing = existingCustomers[0]; -// newCustomer = await CusService.update({ -// sb: req.sb, -// internalCusId: existing.internal_id, -// update: { id, name, email, fingerprint }, -// }); -// } else { -// newCustomer = await createNewCustomer({ -// sb: req.sb, -// orgId: req.orgId, -// env: req.env, -// customer: { -// id, -// name: name || "", -// email: email || "", -// fingerprint, -// }, -// nextResetAt: reset_at, -// logger: req.logtail, -// }); -// } - -// res.status(200).json({ -// customer: CustomerResponseSchema.parse(newCustomer), -// success: true, -// action: existingCustomers.length == 1 ? "update" : "create", -// }); -// } catch (error) { -// handleRequestError({ req, error, res, action: "update customer" }); -// } -// }); diff --git a/server/src/internal/api/customers/cusUtils.ts b/server/src/internal/api/customers/cusUtils.ts index 656659dba..1701d200f 100644 --- a/server/src/internal/api/customers/cusUtils.ts +++ b/server/src/internal/api/customers/cusUtils.ts @@ -284,7 +284,8 @@ export const getCusEntsInFeatures = async ({ const cusEntsWithCusProduct = fullCusProductToCusEnts( fullCusProducts!, - inStatuses + inStatuses, + reverseOrder ); if (!cusEntsWithCusProduct) { @@ -300,7 +301,7 @@ export const getCusEntsInFeatures = async ({ cusEnts = cusEntsWithCusProduct; } - sortCusEntsForDeduction(cusEnts, reverseOrder); + // sortCusEntsForDeduction(cusEnts, reverseOrder); if (!withPrices) { return { cusEnts, cusPrices: undefined }; diff --git a/server/src/internal/api/customers/getCustomerDetails.ts b/server/src/internal/api/customers/getCustomerDetails.ts index 0e6638316..ee1b7e2c0 100644 --- a/server/src/internal/api/customers/getCustomerDetails.ts +++ b/server/src/internal/api/customers/getCustomerDetails.ts @@ -72,17 +72,17 @@ export const getCustomerDetails = async ({ ] ); - let stripeCli = createStripeCli({ - org, - env, - }); - let subs; let subIds = fullCusProducts.flatMap( (cp: FullCusProduct) => cp.subscription_ids ); - if (org.config.api_version >= BREAK_API_VERSION) { + if (org.config.api_version >= BREAK_API_VERSION && org.stripe_connected) { + let stripeCli = createStripeCli({ + org, + env, + }); + subs = await getStripeSubs({ stripeCli, subIds, @@ -106,7 +106,9 @@ export const getCustomerDetails = async ({ org, }); - let features = cusEnts.map((cusEnt: FullCustomerEntitlement) => cusEnt.entitlement.feature); + let features = cusEnts.map( + (cusEnt: FullCustomerEntitlement) => cusEnt.entitlement.feature + ); if (org.api_version == APIVersion.v1_1) { return { @@ -117,7 +119,9 @@ export const getCustomerDetails = async ({ products: main, add_ons: addOns, features: balances.map((b) => { - let isBoolean = features.find((f: Feature) => f.id == b.feature_id)?.type == FeatureType.Boolean; + let isBoolean = + features.find((f: Feature) => f.id == b.feature_id)?.type == + FeatureType.Boolean; if (b.unlimited || isBoolean) { return b; } @@ -128,7 +132,6 @@ export const getCustomerDetails = async ({ included_usage: b.allowance, }); }), - }), }; } else { diff --git a/server/src/internal/api/products/handleUpdateProduct.ts b/server/src/internal/api/products/handleUpdateProduct.ts index 0529d1566..e3d04846d 100644 --- a/server/src/internal/api/products/handleUpdateProduct.ts +++ b/server/src/internal/api/products/handleUpdateProduct.ts @@ -20,7 +20,10 @@ import { handleVersionProduct, handleVersionProductV2, } from "./handleVersionProduct.js"; -import { productsAreDifferent, productsAreDifferent2 } from "@/internal/products/productUtils.js"; +import { + productsAreDifferent, + productsAreDifferent2, +} from "@/internal/products/productUtils.js"; import { routeHandler } from "@/utils/routerUtils.js"; import { handleNewProductItems } from "@/internal/products/product-items/productItemInitUtils.js"; import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; @@ -300,7 +303,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) => // Check if product items are different // let productHasChanged = productsAreDifferent2(req.body, fullProduct); - let itemsExist = notNullish(req.body.items) + let itemsExist = notNullish(req.body.items); if (cusProductExists && itemsExist) { await handleVersionProductV2({ req, diff --git a/server/src/internal/customers/add-product/createFullCusProduct.ts b/server/src/internal/customers/add-product/createFullCusProduct.ts index 18964e830..4f92b5a9a 100644 --- a/server/src/internal/customers/add-product/createFullCusProduct.ts +++ b/server/src/internal/customers/add-product/createFullCusProduct.ts @@ -318,15 +318,6 @@ export const createFullCusProduct = async ({ }); } catch (error) {} - if (!isOneOff(prices) && !product.is_add_on) { - await expireOrDeleteCusProduct({ - sb, - startsAt, - product, - cusProducts: attachParams.cusProducts, - }); - } - const existingCusProduct = searchCusProducts({ productId: product.id, cusProducts: attachParams.cusProducts!, @@ -427,6 +418,16 @@ export const createFullCusProduct = async ({ isCustom: attachParams.isCustom || false, }); + // Expire previous product if not one off + if (!isOneOff(prices) && !product.is_add_on) { + await expireOrDeleteCusProduct({ + sb, + startsAt, + product, + cusProducts: attachParams.cusProducts, + }); + } + await insertFullCusProduct({ sb, cusProd, diff --git a/server/src/internal/customers/add-product/handleAddProduct.ts b/server/src/internal/customers/add-product/handleAddProduct.ts index dea714652..a09bb12c6 100644 --- a/server/src/internal/customers/add-product/handleAddProduct.ts +++ b/server/src/internal/customers/add-product/handleAddProduct.ts @@ -120,6 +120,7 @@ const handleBillNowPrices = async ({ // Add product and entitlements to customer const batchInsert = []; + for (const product of products) { batchInsert.push( createFullCusProduct({ diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index 2e29f8714..417a0d2de 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -79,6 +79,8 @@ export const handleCreateCheckout = async ({ } : undefined; + + const checkout = await stripeCli.checkout.sessions.create({ customer: customer.processor.id, line_items: items, diff --git a/server/src/internal/customers/entitlements/cusEntUtils.ts b/server/src/internal/customers/entitlements/cusEntUtils.ts index e1cec58da..d85233279 100644 --- a/server/src/internal/customers/entitlements/cusEntUtils.ts +++ b/server/src/internal/customers/entitlements/cusEntUtils.ts @@ -190,6 +190,12 @@ export const sortCusEntsForDeduction = ( [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; @@ -234,17 +240,6 @@ export const sortCusEntsForDeduction = ( return 1; } - let nextResetFirst = reverseOrder ? 1 : -1; - // If one 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; - } - // If one has usage_allowed, it should go last if (!a.usage_allowed && b.usage_allowed) { return -1; @@ -254,6 +249,18 @@ export const sortCusEntsForDeduction = ( 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 if (aEnt.interval && bEnt.interval) { @@ -267,6 +274,13 @@ export const sortCusEntsForDeduction = ( // 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}` + // ) + // ); }; // Get related cusPrice diff --git a/server/src/internal/customers/entitlements/cusEntUtils/getExistingUsage.ts b/server/src/internal/customers/entitlements/cusEntUtils/getExistingUsage.ts index a36a5a368..f44a96633 100644 --- a/server/src/internal/customers/entitlements/cusEntUtils/getExistingUsage.ts +++ b/server/src/internal/customers/entitlements/cusEntUtils/getExistingUsage.ts @@ -124,11 +124,13 @@ export const addExistingUsagesToCusEnts = ({ entitlements, curCusProduct, carryExistingUsages = false, + printLogs = false, }: { cusEnts: CustomerEntitlement[]; entitlements: EntitlementWithFeature[]; curCusProduct: FullCusProduct; carryExistingUsages?: boolean; + printLogs?: boolean; }) => { if (!curCusProduct) { return cusEnts; @@ -146,15 +148,17 @@ export const addExistingUsagesToCusEnts = ({ // Sort cusEnts sortCusEntsForDeduction(fullCusEnts); - console.log("DEDUCTING EXISTING USAGE FROM CUS ENTS"); - console.log("Existing usages:", existingUsages); - console.log( - "Sorted cusEnts:", - fullCusEnts.map( - (ce) => - `${ce.entitlement.feature_id} (${ce.entitlement.interval}), balance: ${ce.balance}` - ) - ); + if (printLogs) { + console.log("DEDUCTING EXISTING USAGE FROM CUS ENTS"); + console.log("Existing usages:", existingUsages); + console.log( + "Sorted cusEnts:", + fullCusEnts.map( + (ce) => + `${ce.entitlement.feature_id} (${ce.entitlement.interval}), balance: ${ce.balance}` + ) + ); + } // Perform deductions... for (const key in existingUsages) { @@ -177,7 +181,7 @@ export const addExistingUsagesToCusEnts = ({ if (notNullish(entityUsages)) { // TODO: Check if this works... for (const entityId in entityUsages) { - let { newBalance, toDeduct } = performDeductionOnCusEnt({ + let { toDeduct, newEntities } = performDeductionOnCusEnt({ cusEnt, toDeduct: entityUsages[entityId], allowNegativeBalance: cusEnt.usage_allowed ?? false, @@ -193,7 +197,7 @@ export const addExistingUsagesToCusEnts = ({ }; } - cusEnt.entities![entityId]!.balance = newBalance!; + cusEnt.entities![entityId]!.balance = newEntities![entityId]!.balance; } } else { let { newBalance, toDeduct } = performDeductionOnCusEnt({ @@ -205,10 +209,12 @@ export const addExistingUsagesToCusEnts = ({ cusEnt.balance = newBalance; } - console.log("--------------------------------"); - console.log("Key:", key); - console.log("New cus ent balance:", cusEnt.balance, cusEnt.entities); - console.log("Existing usages:", existingUsages); + if (printLogs) { + console.log("--------------------------------"); + console.log("Key:", key); + console.log("New cus ent balance:", cusEnt.balance, cusEnt.entities); + console.log("Existing usages:", existingUsages); + } } } diff --git a/server/src/internal/customers/products/attachUtils.ts b/server/src/internal/customers/products/attachUtils.ts index 32d3019d5..dc32c8972 100644 --- a/server/src/internal/customers/products/attachUtils.ts +++ b/server/src/internal/customers/products/attachUtils.ts @@ -20,7 +20,6 @@ import { ErrCode } from "@/errors/errCodes.js"; import RecaseError from "@/utils/errorUtils.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; -import { PricesInput } from "@autumn/shared"; import { notNullish, nullish } from "@/utils/genUtils.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; diff --git a/server/src/internal/customers/products/cusProductUtils.ts b/server/src/internal/customers/products/cusProductUtils.ts index 4a4de30e3..b841201cf 100644 --- a/server/src/internal/customers/products/cusProductUtils.ts +++ b/server/src/internal/customers/products/cusProductUtils.ts @@ -272,7 +272,8 @@ export const activateFutureProduct = async ({ // GET CUS ENTS FROM CUS PRODUCTS export const fullCusProductToCusEnts = ( cusProducts: FullCusProduct[], - inStatuses: CusProductStatus[] = [CusProductStatus.Active] + inStatuses: CusProductStatus[] = [CusProductStatus.Active], + reverseOrder: boolean = false ) => { const cusEnts: FullCustomerEntitlement[] = []; @@ -289,7 +290,7 @@ export const fullCusProductToCusEnts = ( ); } - sortCusEntsForDeduction(cusEnts); + sortCusEntsForDeduction(cusEnts, reverseOrder); return cusEnts; }; diff --git a/server/src/internal/products/product-items/mapFromItem.ts b/server/src/internal/products/product-items/mapFromItem.ts index 9b0647f5e..9beed45f1 100644 --- a/server/src/internal/products/product-items/mapFromItem.ts +++ b/server/src/internal/products/product-items/mapFromItem.ts @@ -92,6 +92,8 @@ export const toFeature = ({ }) => { let isBoolean = feature?.type == FeatureType.Boolean; + let resetUsage = item.reset_usage_when_enabled || false; + let ent: Entitlement = { id: item.entitlement_id || generateId("ent"), org_id: orgId, @@ -113,7 +115,7 @@ export const toFeature = ({ ? EntInterval.Lifetime : (itemToEntInterval(item) as EntInterval), - carry_from_previous: item.carry_over_usage || false, + carry_from_previous: !resetUsage, entity_feature_id: item.entity_feature_id, }; @@ -146,6 +148,7 @@ export const toFeatureAndPrice = ({ curEnt?: Entitlement; newVersion?: boolean; }) => { + let resetUsage = item.reset_usage_when_enabled || false; let ent: Entitlement = { id: item.entitlement_id || generateId("ent"), org_id: orgId, @@ -163,7 +166,7 @@ export const toFeatureAndPrice = ({ ? EntInterval.Lifetime : (itemToEntInterval(item) as EntInterval), - carry_from_previous: item.carry_over_usage || false, + carry_from_previous: !resetUsage, entity_feature_id: item.entity_feature_id, }; diff --git a/server/src/internal/products/product-items/mapToItem.ts b/server/src/internal/products/product-items/mapToItem.ts index 6bf5f8220..dcfc3435c 100644 --- a/server/src/internal/products/product-items/mapToItem.ts +++ b/server/src/internal/products/product-items/mapToItem.ts @@ -12,6 +12,7 @@ import { ProductItemInterval, TierInfinite, UsagePriceConfig, + Product, } from "@autumn/shared"; import { nullish } from "@/utils/genUtils.js"; @@ -48,7 +49,7 @@ export const toFeatureItem = ({ ent }: { ent: EntitlementWithFeature }) => { interval: entToItemInterval(ent.interval!), entity_feature_id: ent.entity_feature_id, - carry_over_usage: ent.carry_from_previous, + reset_usage_when_enabled: !ent.carry_from_previous, // Stored in backend entitlement_id: ent.id, @@ -73,8 +74,7 @@ export const toFeaturePriceItem = ({ }; }); - - return { + let item: ProductItem = { feature_id: ent.feature.id, included_usage: ent.allowance, interval: billingToItemInterval(config.interval!), @@ -86,7 +86,7 @@ export const toFeaturePriceItem = ({ billing_units: config.billing_units, entity_feature_id: ent.entity_feature_id, - carry_over_usage: ent.carry_from_previous, + reset_usage_when_enabled: !ent.carry_from_previous, usage_model: config.bill_when == BillWhen.StartOfPeriod || config.bill_when == BillWhen.InAdvance @@ -100,6 +100,8 @@ export const toFeaturePriceItem = ({ price_config: price.config, }; + + return item; }; export const toPriceItem = ({ price }: { price: Price }) => { diff --git a/server/src/trigger/updateBalanceTask.ts b/server/src/trigger/updateBalanceTask.ts index 4bac1e679..833454567 100644 --- a/server/src/trigger/updateBalanceTask.ts +++ b/server/src/trigger/updateBalanceTask.ts @@ -1,6 +1,4 @@ -import { createSupabaseClient } from "@/external/supabaseUtils.js"; -import { handleBelowThresholdInvoicing } from "./invoiceThresholdUtils.js"; -import { getBelowThresholdPrice } from "./invoiceThresholdUtils.js"; + import { AllowanceType, @@ -543,6 +541,8 @@ export const updateCustomerBalance = async ({ }) => { const startTime = performance.now(); console.log("REVERSE DEDUCTION ORDER", org.config.reverse_deduction_order); + + const { cusEnts, cusPrices } = await getCusEntsInFeatures({ sb, internalCustomerId: customer.internal_id, diff --git a/server/src/trigger/updateUsageTask.ts b/server/src/trigger/updateUsageTask.ts index b011e83f9..d8a1e8a56 100644 --- a/server/src/trigger/updateUsageTask.ts +++ b/server/src/trigger/updateUsageTask.ts @@ -198,6 +198,7 @@ export const updateUsage = async ({ inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue], withPrices: true, logger, + reverseOrder: org.config?.reverse_deduction_order, }); const endTime = performance.now(); diff --git a/server/test.sh b/server/test.sh index d6a0d20ca..942132246 100755 --- a/server/test.sh +++ b/server/test.sh @@ -5,10 +5,12 @@ MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts" # TEST PARALLEL if [ "$1" == "basic-parallel" ]; then MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \ - tests/basic/*.ts \ - tests/basic/multi-feature/*.ts \ - tests/basic/entities/*.ts \ - # tests/basic/referrals/*.ts \ + tests/basic/referrals/*.ts \ + tests/attach/**/*.ts \ + # tests/basic/*.ts \ + # tests/basic/multi-feature/*.ts \ + # tests/basic/entities/*.ts \ + # && $MOCHA_CMD tests/basic/referrals/*.ts \ # tests/attach/**/*.ts \ elif [ "$1" == "advanced-parallel" ]; then diff --git a/shared/models/cusModels/cusEntModels/cusEntitlementModels.ts b/shared/models/cusModels/cusEntModels/cusEntitlementModels.ts index bf29437e8..6271a5309 100644 --- a/shared/models/cusModels/cusEntModels/cusEntitlementModels.ts +++ b/shared/models/cusModels/cusEntModels/cusEntitlementModels.ts @@ -16,7 +16,7 @@ export const CustomerEntitlementSchema = z.object({ id: z.string(), internal_customer_id: z.string(), internal_feature_id: z.string(), - customer_id: z.string(), // for debugging purposes + customer_id: z.string().nullish(), // for debugging purposes feature_id: z.string(), // for debugging purposes customer_product_id: z.string(), diff --git a/shared/models/cusModels/cusResponseModels.ts b/shared/models/cusModels/cusResponseModels.ts index b411d191b..333c38734 100644 --- a/shared/models/cusModels/cusResponseModels.ts +++ b/shared/models/cusModels/cusResponseModels.ts @@ -3,7 +3,7 @@ import { CusProductStatus } from "./cusProductModels.js"; import { EntInterval } from "../genModels.js"; export const CusProductResponseSchema = z.object({ - id: z.string(), + id: z.string().nullable().default(null), name: z.string(), group: z.string().nullable(), status: z.nativeEnum(CusProductStatus), @@ -19,9 +19,9 @@ export const CusProductResponseSchema = z.object({ export const CusEntResponseSchema = z.object({ feature_id: z.string(), - interval: z.nativeEnum(EntInterval).nullish(), + interval: z.nativeEnum(EntInterval).nullish(), unlimited: z.boolean().nullish(), - balance: z.number().nullish(), // + balance: z.number().nullish(), // usage: z.number().nullish(), included_usage: z.number().nullish(), next_reset_at: z.number().nullish(), @@ -29,7 +29,7 @@ export const CusEntResponseSchema = z.object({ export const CusResponseSchema = z.object({ autumn_id: z.string(), - id: z.string(), + id: z.string().nullable().default(null), name: z.string().nullable(), email: z.string().nullable(), fingerprint: z.string().nullable(), diff --git a/shared/models/productModels/productItemModels.ts b/shared/models/productModels/productItemModels.ts index 459e8c71e..74ac079eb 100644 --- a/shared/models/productModels/productItemModels.ts +++ b/shared/models/productModels/productItemModels.ts @@ -70,7 +70,8 @@ export const ProductItemSchema = z.object({ // Others entity_feature_id: z.string().nullish(), - carry_over_usage: z.boolean().nullish(), + // carry_over_usage: z.boolean().nullish(), + reset_usage_when_enabled: z.boolean().nullish(), // Stored in backend created_at: z.number().nullish(), diff --git a/vite/src/components/ui/dialog.tsx b/vite/src/components/ui/dialog.tsx index f499df236..0fbcaf2e9 100644 --- a/vite/src/components/ui/dialog.tsx +++ b/vite/src/components/ui/dialog.tsx @@ -30,21 +30,21 @@ function DialogClose({ return ; } -function DialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = "DialogOverlay"; function DialogContent({ className, diff --git a/vite/src/main.tsx b/vite/src/main.tsx index 120a29209..71d793a67 100644 --- a/vite/src/main.tsx +++ b/vite/src/main.tsx @@ -11,9 +11,7 @@ if (!PUBLISHABLE_KEY) { throw new Error("Add your Clerk Publishable Key to the .env file"); } -// posthog.init("phc_L763VL2WY6TExI5xG5xWOua6E5LYkZN496hBeSpEdAn", { -// api_host: "https://us.i.posthog.com", -// }); + const options = { api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, diff --git a/vite/src/utils/product/product-item/validateProductItem.ts b/vite/src/utils/product/product-item/validateProductItem.ts new file mode 100644 index 000000000..377f88501 --- /dev/null +++ b/vite/src/utils/product/product-item/validateProductItem.ts @@ -0,0 +1,84 @@ +import { invalidNumber, notNullish } from "@/utils/genUtils"; +import { ProductItem } from "@autumn/shared"; +import { toast } from "sonner"; + +export const validateProductItem = (item: ProductItem, show: any) => { + // Price item validation (when amount is set) + + console.log("item", item); + if (notNullish(item.price) && show.price) { + if (invalidNumber(item.price)) { + toast.error("Please enter a valid price amount"); + return null; + } + item.price = parseFloat(item.price!.toString()); + } + + if ((item.included_usage as any) === "") { + item.included_usage = null; + } else if (!invalidNumber(item.included_usage)) { + item.included_usage = Number(item.included_usage); + } + + //if both item.tiers and item.price are set, set item.price to null + if (item.tiers && item.price) { + item.price = null; + } + + // Usage/Feature item validation (when tiers are set) + if (item.tiers) { + let previousTo = 0; + + for (let i = 0; i < item.tiers.length; i++) { + const tier = item.tiers[i]; + + // Check if amount is actually a number + if (typeof tier.amount !== "number") { + tier.amount = parseFloat(tier.amount); + } + + // Check if amount is valid + if (invalidNumber(tier.amount)) { + toast.error("Please enter valid prices for all tiers"); + return null; + } + + // Check if amount is negative + if (tier.amount < 0) { + toast.error("Please set a positive usage price"); + return null; + } + + // Skip other validations if 'to' is "inf" + if (tier.to === "inf") { + continue; + } + + tier.to = Number(tier.to); + + // Check if 'to' is a number and valid + if (typeof tier.to !== "number" || invalidNumber(tier.to)) { + toast.error("Please enter valid usage limits for all tiers"); + return null; + } + + // Ensure tiers are in ascending order + if (tier.to <= previousTo) { + toast.error("Tiers must be in ascending order"); + return null; + } + + previousTo = tier.to; + } + } + + // Validate billing units + if (item.billing_units && invalidNumber(item.billing_units)) { + toast.error("Please enter valid billing units"); + return null; + } else { + item.billing_units = Number(item.billing_units); + } + + return item; +}; diff --git a/vite/src/views/customers/CreateCustomer.tsx b/vite/src/views/customers/CreateCustomer.tsx index 6c7da0b85..bcd2e7647 100644 --- a/vite/src/views/customers/CreateCustomer.tsx +++ b/vite/src/views/customers/CreateCustomer.tsx @@ -34,17 +34,16 @@ function CreateCustomer() { setIsLoading(true); try { - const {data} = await CusService.createCustomer(axiosInstance, { + const { data } = await CusService.createCustomer(axiosInstance, { ...fields, id: fields.id ? fields.id : null, fingerprint: fields.fingerprint ? fields.fingerprint : undefined, }); let customer = data.customer || data; - console.log(customer); if (customer) { navigateTo( - `/customers/${customer.id || customer.internal_id}`, + `/customers/${customer.id || customer.autumn_id}`, navigate, env ); diff --git a/vite/src/views/customers/customer/CustomerDetails.tsx b/vite/src/views/customers/customer/CustomerDetails.tsx index 5a646a33f..fa4668e36 100644 --- a/vite/src/views/customers/customer/CustomerDetails.tsx +++ b/vite/src/views/customers/customer/CustomerDetails.tsx @@ -5,26 +5,18 @@ import { } from "@/components/ui/tooltip"; import { useCustomerContext } from "./CustomerContext"; import { getStripeCusLink } from "@/utils/linkUtils"; -import { Product } from "@autumn/shared"; + import { faStripe } from "@fortawesome/free-brands-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ArrowUpRightFromSquare, Check } from "lucide-react"; -import { Copy } from "lucide-react"; import { useState } from "react"; import { Link } from "react-router"; import { SideAccordion } from "@/components/general/SideAccordion"; import { Accordion } from "@/components/ui/accordion"; import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import React from "react"; +import { Dialog } from "@/components/ui/dialog"; + import UpdateCustomerDialog from "./UpdateCustomerDialog"; import AddCouponDialogContent from "./add-coupon/AddCouponDialogContent"; import CopyButton from "@/components/general/CopyButton"; @@ -194,7 +186,6 @@ export const RewardProps = () => { return coupon.name; }; let { referrals } = useCustomerContext(); - console.log("referrals", referrals); // if (!referrals) return null; diff --git a/vite/src/views/customers/customer/entitlements/CustomerEntitlementsList.tsx b/vite/src/views/customers/customer/entitlements/CustomerEntitlementsList.tsx index ee4341774..4aa235da6 100644 --- a/vite/src/views/customers/customer/entitlements/CustomerEntitlementsList.tsx +++ b/vite/src/views/customers/customer/entitlements/CustomerEntitlementsList.tsx @@ -1,12 +1,3 @@ -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; - import { AllowanceType, FeatureType, @@ -15,7 +6,6 @@ import { import { useCustomerContext } from "../CustomerContext"; import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils"; -import { compareStatus } from "@/utils/genUtils"; import { useState } from "react"; diff --git a/vite/src/views/customers/customer/entitlements/UpdateCusEntitlement.tsx b/vite/src/views/customers/customer/entitlements/UpdateCusEntitlement.tsx index 2a686527a..997f297cd 100644 --- a/vite/src/views/customers/customer/entitlements/UpdateCusEntitlement.tsx +++ b/vite/src/views/customers/customer/entitlements/UpdateCusEntitlement.tsx @@ -18,7 +18,7 @@ import { DateInputUnix } from "@/components/general/DateInputUnix"; import { useAxiosInstance } from "@/services/useAxiosInstance"; import { CusService } from "@/services/customers/CusService"; import { toast } from "sonner"; -import { getBackendErr } from "@/utils/genUtils"; +import { getBackendErr, notNullish } from "@/utils/genUtils"; import CopyButton from "@/components/general/CopyButton"; function UpdateCusEntitlement({ @@ -102,7 +102,9 @@ function UpdateCusEntitlement({ Balance { setUpdateFields({ ...updateFields, diff --git a/vite/src/views/products/product/product-item/CreateProductItem.tsx b/vite/src/views/products/product/product-item/CreateProductItem.tsx index 7e4439cd7..40b09140f 100644 --- a/vite/src/views/products/product/product-item/CreateProductItem.tsx +++ b/vite/src/views/products/product/product-item/CreateProductItem.tsx @@ -16,6 +16,7 @@ import { Feature, ProductItemInterval, ProductItem } from "@autumn/shared"; import { useProductContext } from "../ProductContext"; import { toast } from "sonner"; import { invalidNumber } from "@/utils/genUtils"; +import { validateProductItem } from "@/utils/product/product-item/validateProductItem"; export let defaultProductItem: ProductItem = { feature_id: null, @@ -31,7 +32,7 @@ export let defaultProductItem: ProductItem = { // Others entity_feature_id: null, - carry_over_usage: false, + reset_usage_when_enabled: false, }; let defaultPriceItem: ProductItem = { @@ -48,7 +49,7 @@ let defaultPriceItem: ProductItem = { // Others entity_feature_id: null, - carry_over_usage: false, + reset_usage_when_enabled: false, }; export function CreateProductItem() { @@ -57,8 +58,6 @@ export function CreateProductItem() { const [item, setItem] = useState(defaultProductItem); const { features, product, setProduct, setFeatures } = useProductContext(); - console.log(item); - const setSelectedFeature = (feature: Feature) => { setFeatures([...features, feature]); setItem({ ...item, feature_id: feature.id! }); @@ -145,82 +144,3 @@ export function CreateProductItem() { ); } - -export const validateProductItem = (item: ProductItem, show: any) => { - // Price item validation (when amount is set) - if (item.price !== null && show.price) { - if (invalidNumber(item.price)) { - toast.error("Please enter a valid price amount"); - return null; - } - item.price = parseFloat(item.price!.toString()); - } - - if ((item.included_usage as any) === "") { - item.included_usage = null; - } else if (!invalidNumber(item.included_usage)) { - item.included_usage = Number(item.included_usage); - } - - //if both item.tiers and item.price are set, set item.price to null - if (item.tiers && item.price) { - item.price = null; - } - - // Usage/Feature item validation (when tiers are set) - if (item.tiers) { - let previousTo = 0; - - for (let i = 0; i < item.tiers.length; i++) { - const tier = item.tiers[i]; - - // Check if amount is actually a number - if (typeof tier.amount !== "number") { - tier.amount = parseFloat(tier.amount); - } - - // Check if amount is valid - if (invalidNumber(tier.amount)) { - toast.error("Please enter valid prices for all tiers"); - return null; - } - - // Check if amount is negative - if (tier.amount < 0) { - toast.error("Please set a positive usage price"); - return null; - } - - // Skip other validations if 'to' is "inf" - if (tier.to === "inf") { - continue; - } - - tier.to = Number(tier.to); - - // Check if 'to' is a number and valid - if (typeof tier.to !== "number" || invalidNumber(tier.to)) { - toast.error("Please enter valid usage limits for all tiers"); - return null; - } - - // Ensure tiers are in ascending order - if (tier.to <= previousTo) { - toast.error("Tiers must be in ascending order"); - return null; - } - - previousTo = tier.to; - } - } - - // Validate billing units - if (item.billing_units && invalidNumber(item.billing_units)) { - toast.error("Please enter valid billing units"); - return null; - } else { - item.billing_units = Number(item.billing_units); - } - - return item; -}; diff --git a/vite/src/views/products/product/product-item/MoreMenuButton.tsx b/vite/src/views/products/product/product-item/MoreMenuButton.tsx index 02e017471..70843c74f 100644 --- a/vite/src/views/products/product/product-item/MoreMenuButton.tsx +++ b/vite/src/views/products/product/product-item/MoreMenuButton.tsx @@ -9,7 +9,7 @@ import { import { EllipsisVertical, MinusIcon, PlusIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useProductItemContext } from "./ProductItemContext"; -import { UsageModel } from "@autumn/shared"; +import { ProductItem, UsageModel } from "@autumn/shared"; export default function MoreMenuButton({ show, @@ -19,18 +19,22 @@ export default function MoreMenuButton({ setShow: (show: any) => void; }) { const [showPopover, setShowPopover] = useState(false); - const { item, setItem } = useProductItemContext(); + const { + item, + setItem, + }: { item: ProductItem; setItem: (item: ProductItem) => void } = + useProductItemContext(); - useEffect(() => { - const shouldCarryOver = - item.interval === null || item.reset_usage_on_billing === false; - if (item.carry_from_previous !== shouldCarryOver) { - setItem({ - ...item, - carry_from_previous: shouldCarryOver, - }); - } - }, [item.interval, item.reset_usage_on_billing]); + // useEffect(() => { + // const shouldCarryOver = + // item.interval === null || item.reset_usage_on_billing === false; + // if (item.carry_over_usage !== shouldCarryOver) { + // setItem({ + // ...item, + // carry_over_usage: shouldCarryOver, + // }); + // } + // }, [item.interval, item.reset_usage_on_billing]); return ( @@ -49,27 +53,6 @@ export default function MoreMenuButton({ className="w-fit min-w-48 p-0 py-1 flex flex-col text-xs" align="end" > - {/*
- -
*/}