diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index 56a5e87d4..1c1996245 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -435,17 +435,16 @@ export class AutumnInt { stripe = { connect: async (params: { - testApiKey: string; - liveApiKey: string; - successUrl: string; - defaultCurrency: string; + secret_key: string; + success_url: string; + default_currency: string; }) => { - const data = await this.post(`/org/stripe`, params); + const data = await this.post(`/organization/stripe`, params); return data; }, delete: async () => { - const data = await this.delete(`/org/stripe`); + const data = await this.delete(`/organization/stripe`); return data; }, }; diff --git a/server/src/internal/api/apiRouter.ts b/server/src/internal/api/apiRouter.ts index 991c4e5b7..2ee728cc0 100644 --- a/server/src/internal/api/apiRouter.ts +++ b/server/src/internal/api/apiRouter.ts @@ -67,7 +67,9 @@ apiRouter.post("/setup_payment", handleSetupPayment); // Analytics apiRouter.use("/query", analyticsRouter); apiRouter.use("/platform", platformRouter); + +// Used for tests... apiRouter.post("/organization/stripe", handleConnectStripe); -// apiRouter.delete("/org/stripe", handleDeleteStripe); +apiRouter.delete("/organization/stripe", handleDeleteStripe); export { apiRouter }; diff --git a/server/src/internal/api/entitled/checkUtils/getV1CheckResponse.ts b/server/src/internal/api/entitled/checkUtils/getV1CheckResponse.ts index b109a5051..4a0d232d8 100644 --- a/server/src/internal/api/entitled/checkUtils/getV1CheckResponse.ts +++ b/server/src/internal/api/entitled/checkUtils/getV1CheckResponse.ts @@ -2,6 +2,7 @@ import { getFeatureBalance, cusEntsContainFeature, getUnlimitedAndUsageAllowed, + getPaidFeatureBalance, } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js"; import { @@ -88,6 +89,8 @@ export const getV1CheckResponse = ({ internalFeatureId: feature.internal_id!, }); + console.log(`Feature ${feature.id}, Usage allowed: ${usageAllowed}`); + if (unlimited || usageAllowed) { balances.push({ feature_id: feature.id, @@ -103,7 +106,6 @@ export const getV1CheckResponse = ({ }), }); allowed = true; - // continue; break; } @@ -116,6 +118,11 @@ export const getV1CheckResponse = ({ entityId, }); + let totalPaidAllowance = getPaidFeatureBalance({ + cusEnts, + internalFeatureId: feature.internal_id!, + }); + let newBalance: any = { feature_id: feature.id, required, @@ -129,7 +136,7 @@ export const getV1CheckResponse = ({ balances.push(newBalance); // allowed = allowed && actual! >= required; - allowed = actual! >= required; + allowed = actual! + (totalPaidAllowance || 0) >= required; if (allowed) { break; diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts index 5497cbacb..b95b9a000 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts @@ -32,6 +32,7 @@ import { getSummedEntityBalances, } from "./entBalanceUtils.js"; import { Decimal } from "decimal.js"; +import { logger } from "better-auth"; export const getCusEntMasterBalance = ({ cusEnt, @@ -343,9 +344,11 @@ export const getResetBalance = ({ export const getUnlimitedAndUsageAllowed = ({ cusEnts, internalFeatureId, + includeUsageLimit = true, }: { cusEnts: FullCustomerEntitlement[]; internalFeatureId: string; + includeUsageLimit?: boolean; }) => { // Unlimited @@ -360,7 +363,7 @@ export const getUnlimitedAndUsageAllowed = ({ (ent) => ent.internal_feature_id === internalFeatureId && ent.usage_allowed && - nullish(ent.entitlement.usage_limit) + (includeUsageLimit ? nullish(ent.entitlement.usage_limit) : true) ); return { unlimited, usageAllowed }; @@ -424,6 +427,32 @@ export const getFeatureBalance = ({ return balance; }; +export const getPaidFeatureBalance = ({ + cusEnts, + internalFeatureId, +}: { + cusEnts: FullCustomerEntitlement[]; + internalFeatureId: string; +}) => { + let paidAllowance = 0; + try { + for (const cusEnt of cusEnts) { + if (cusEnt.internal_feature_id !== internalFeatureId) continue; + + if (notNullish(cusEnt.entitlement.usage_limit)) { + paidAllowance = new Decimal(paidAllowance) + .plus(cusEnt.entitlement.usage_limit!) + .minus(cusEnt.entitlement.allowance || 0) + .toNumber(); + } + } + } catch (error) { + logger.error(`Failed to get paid feature balance`, { error }); + } + + return paidAllowance; +}; + export const cusEntsContainFeature = ({ cusEnts, feature, diff --git a/server/src/trigger/updateUsageTask.ts b/server/src/trigger/updateUsageTask.ts index 746e59268..bff3bbd7b 100644 --- a/server/src/trigger/updateUsageTask.ts +++ b/server/src/trigger/updateUsageTask.ts @@ -11,7 +11,10 @@ import { import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js"; import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js"; -import { getFeatureBalance } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; +import { + getFeatureBalance, + getPaidFeatureBalance, +} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; import { Decimal } from "decimal.js"; import { diff --git a/server/tests/utils/setup.ts b/server/tests/utils/setup.ts index d625ec433..fdcca2442 100644 --- a/server/tests/utils/setup.ts +++ b/server/tests/utils/setup.ts @@ -86,10 +86,9 @@ export const clearOrg = async ({ } catch (error) {} try { await autumn.stripe.connect({ - testApiKey: process.env.STRIPE_TEST_KEY!, - liveApiKey: process.env.STRIPE_TEST_KEY!, - successUrl: "https://useautumn.com", - defaultCurrency: "usd", + secret_key: process.env.STRIPE_TEST_KEY!, + success_url: "https://useautumn.com", + default_currency: "usd", }); } catch (error: any) { console.error("Error reconnecting stripe", error.message); diff --git a/vite/src/utils/formatUtils/formatDateUtils.ts b/vite/src/utils/formatUtils/formatDateUtils.ts index 143269e27..8cb7e4331 100644 --- a/vite/src/utils/formatUtils/formatDateUtils.ts +++ b/vite/src/utils/formatUtils/formatDateUtils.ts @@ -11,10 +11,15 @@ export const formatTimestamp = (timestamp: number | null | undefined) => { export const formatUnixToDate = ( unix: number | null | undefined, - excludeYear = false, + excludeYear = false ) => { if (!unix) return ""; - return format(new Date(unix), excludeYear ? "d MMM" : "d MMM yyyy"); + try { + const date = format(new Date(unix), excludeYear ? "d MMM" : "d MMM yyyy"); + return date; + } catch (error) { + return ""; + } }; export const formatUnixToDateTime = (unix: number | null | undefined) => { diff --git a/vite/src/views/customers/customer/product/components/attach-preview/AttachInfo.tsx b/vite/src/views/customers/customer/product/components/attach-preview/AttachInfo.tsx index ff976d54f..ce16a16cc 100644 --- a/vite/src/views/customers/customer/product/components/attach-preview/AttachInfo.tsx +++ b/vite/src/views/customers/customer/product/components/attach-preview/AttachInfo.tsx @@ -45,20 +45,21 @@ export const AttachInfo = () => { ); }); + let text = `You are switching this customer to version ${product.version} of ${product.name}.`; + if (preview.due_next_cycle?.due_at) { + text += `Their features will update immediately and from ${formatUnixToDate(preview.due_next_cycle?.due_at)} onwards, they will pay any new prices${usagePriceExists ? " (including usage from the last cycle)" : ""}.`; + } + return ( <> - You are switching this customer to version {product.version} of{" "} - {product.name}. Their features will update immediately and from{" "} - {format(preview.due_next_cycle?.due_at, "d MMM")} onwards, they will - pay any new prices - {usagePriceExists ? " (including usage from the last cycle)" : ""}. + {/* You are switching this customer to version {product.version} of{" "} + {product.name}. */} + {text} ); - const text = `You are switching this customer to version ${product.version} of ${product.name}.`; - // let text = `The customer is currently on ${currentProduct.name} v${currentProduct.version}. Switching to v${product.version} will update the customer's features immediately, and from ${formatUnixToDate(preview.due_next_cycle.due_at)} onwards they will pay any new prices`; // if (usagePriceExists) {