From 1893c160496ac7abd3859f858f0cedaed5e44c37 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 18 Apr 2025 13:49:05 +0100 Subject: [PATCH] Refactor referral code handling by removing unnecessary console logs and improving error handling in the referral code fetching process. Update test script to include referral tests and adjust product item validation logic for better clarity and functionality. --- example/src/app/referrals/functions.tsx | 3 +- example/src/app/referrals/page.tsx | 1 - .../src/internal/api/customers/cusRouter.ts | 103 ------------------ server/src/internal/api/customers/cusUtils.ts | 5 +- .../api/customers/getCustomerDetails.ts | 21 ++-- .../api/products/handleUpdateProduct.ts | 7 +- .../add-product/createFullCusProduct.ts | 19 ++-- .../customers/add-product/handleAddProduct.ts | 1 + .../add-product/handleCreateCheckout.ts | 2 + .../customers/entitlements/cusEntUtils.ts | 36 ++++-- .../cusEntUtils/getExistingUsage.ts | 36 +++--- .../customers/products/attachUtils.ts | 1 - .../customers/products/cusProductUtils.ts | 5 +- .../products/product-items/mapFromItem.ts | 7 +- .../products/product-items/mapToItem.ts | 10 +- server/src/trigger/updateBalanceTask.ts | 6 +- server/src/trigger/updateUsageTask.ts | 1 + server/test.sh | 10 +- .../cusEntModels/cusEntitlementModels.ts | 2 +- shared/models/cusModels/cusResponseModels.ts | 8 +- .../models/productModels/productItemModels.ts | 3 +- vite/src/components/ui/dialog.tsx | 30 ++--- vite/src/main.tsx | 4 +- .../product-item/validateProductItem.ts | 84 ++++++++++++++ vite/src/views/customers/CreateCustomer.tsx | 5 +- .../customers/customer/CustomerDetails.tsx | 15 +-- .../entitlements/CustomerEntitlementsList.tsx | 10 -- .../entitlements/UpdateCusEntitlement.tsx | 6 +- .../product-item/CreateProductItem.tsx | 86 +-------------- .../product/product-item/MoreMenuButton.tsx | 63 ++++------- .../product-item/ProductItemConfig.tsx | 55 ---------- .../product/product-item/ProductItemTable.tsx | 4 +- .../product-item/UpdateProductItem.tsx | 61 ++++++----- .../components/ConfigWithFeature.tsx | 2 +- .../product/versioning/CountAndMigrate.tsx | 3 +- 35 files changed, 284 insertions(+), 431 deletions(-) create mode 100644 vite/src/utils/product/product-item/validateProductItem.ts 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" > - {/*
- -
*/}