diff --git a/bun.lock b/bun.lock index 4ccefda05..fa2cd259a 100644 --- a/bun.lock +++ b/bun.lock @@ -152,7 +152,6 @@ "dependencies": { "@autumn/shared": "workspace:*", "@better-auth/stripe": "^1.2.12", - "@clerk/clerk-react": "^5.24.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@heroicons/react": "^2.2.0", @@ -327,8 +326,6 @@ "@clerk/backend": ["@clerk/backend@2.9.1", "", { "dependencies": { "@clerk/shared": "^3.21.0", "@clerk/types": "^4.78.0", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-ZRvy4cHpCrOORtyB3xixifek5/Tsc+X5pkC4vVBrZyqntcRsqhO58eSwb4O4rba6gE9UpVhTYnP1+DVLLiyTJw=="], - "@clerk/clerk-react": ["@clerk/clerk-react@5.42.1", "", { "dependencies": { "@clerk/shared": "^3.21.0", "@clerk/types": "^4.78.0", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-6CuOY0QttPU2uimzTTVEEeH0mKo4xQVhaUN8Iqe+Z6nIwYLOTa2w4Lk8wAqEQHWbd1WBOAkP3b+7c/QToWTH7A=="], - "@clerk/express": ["@clerk/express@1.7.21", "", { "dependencies": { "@clerk/backend": "^2.9.1", "@clerk/shared": "^3.21.0", "@clerk/types": "^4.78.0", "tslib": "2.8.1" }, "peerDependencies": { "express": "^4.17.0 || ^5.0.0" } }, "sha512-ALtBBhaYU+gfRoJp25hC3OntbNNgcrvLSyIYVShSCQ8gmyFjrJe/u6wb2PJalKtiE0SJUZPCAPCr8QpWSXEIsg=="], "@clerk/shared": ["@clerk/shared@3.21.0", "", { "dependencies": { "@clerk/types": "^4.78.0", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-BlIjPLsw2ral0V2sDDTNxoSK3SLLT6tfKiTJG2I3vQILnrGPEwkiEaMY1+bJtOze7EHdpP2KSsTzzjFTP1UDOg=="], @@ -1091,7 +1088,7 @@ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], + "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], "@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="], @@ -3061,7 +3058,7 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@types/bun/bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="], + "@types/bun/bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], diff --git a/scripts/start-dev.js b/scripts/start-dev.js index 7ed2ff58f..4b035bcc2 100644 --- a/scripts/start-dev.js +++ b/scripts/start-dev.js @@ -1,10 +1,51 @@ import { spawn } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; import { detectAndSetPorts } from "./detect-ports.js"; +/** + * Read environment variable from .env file + */ +function getEnvVariable(filePath, key) { + if (!fs.existsSync(filePath)) { + return null; + } + const content = fs.readFileSync(filePath, "utf-8"); + const lines = content.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith("#")) { + const [envKey, ...valueParts] = trimmed.split("="); + if (envKey && envKey.trim() === key) { + return valueParts.join("="); + } + } + } + return null; +} + async function startDev() { try { - // Detect and set ports - const { vitePort, serverPort } = await detectAndSetPorts(); + // Check if using remote backend (api.useautumn.com) + const rootDir = path.dirname(new URL(import.meta.url).pathname); + const projectRoot = path.join(rootDir, ".."); + const viteEnvPath = path.join(projectRoot, "vite", ".env"); + const backendUrl = + process.env.VITE_BACKEND_URL || getEnvVariable(viteEnvPath, "VITE_BACKEND_URL"); + const isUsingRemoteBackend = backendUrl && backendUrl.includes("api.useautumn.com"); + + let vitePort = 3000; + let serverPort = 8080; + + if (isUsingRemoteBackend) { + console.log("\n🌐 Using remote backend (api.useautumn.com)"); + console.log("ā­ļø Skipping port detection...\n"); + } else { + // Detect and set ports + const ports = await detectAndSetPorts(); + vitePort = ports.vitePort; + serverPort = ports.serverPort; + } // Step 1: Build shared package first (initial build) console.log("\nšŸ“¦ Building shared package...\n"); diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts index 60ee39ee7..30ce229dd 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts @@ -1,39 +1,34 @@ -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; - import { - AppEnv, + type AppEnv, BillingType, CusProductStatus, - Customer, - FullCusProduct, - FullCustomerEntitlement, - FullCustomerPrice, - Organization, + type Customer, + type FullCusProduct, + type FullCustomerEntitlement, + type FullCustomerPrice, + type Organization, } from "@autumn/shared"; -import Stripe from "stripe"; - -import { createStripeCli } from "../../utils.js"; -import { getStripeSubs } from "../../stripeSubUtils.js"; -import { getBillingType } from "@/internal/products/prices/priceUtils.js"; -import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; - -import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js"; -import { notNullish } from "@/utils/genUtils.js"; - +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { EntityService } from "@/internal/api/entities/EntityService.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; +import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js"; +import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; -import { getFeatureName } from "@/internal/features/utils/displayUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; +import { getBillingType } from "@/internal/products/prices/priceUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; import { getFullStripeInvoice, invoiceToSubId, } from "../../stripeInvoiceUtils.js"; -import { handleUsagePrices } from "./handleUsagePrices.js"; -import { handleContUsePrices } from "./handleContUsePrices.js"; -import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; -import { handlePrepaidPrices } from "./handlePrepaidPrices.js"; -import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js"; +import { getStripeSubs } from "../../stripeSubUtils.js"; +import { createStripeCli } from "../../utils.js"; +import { handleContUsePrices } from "./handleContUsePrices.js"; +import { handlePrepaidPrices } from "./handlePrepaidPrices.js"; +import { handleUsagePrices } from "./handleUsagePrices.js"; const handleInArrearProrated = async ({ db, @@ -82,19 +77,19 @@ const handleInArrearProrated = async ({ return; } - let feature = cusEnt.entitlement.feature; + const feature = cusEnt.entitlement.feature; logger.info( `Handling invoice.created for in arrear prorated, feature: ${feature.id}`, ); - let deletedEntities = await EntityService.list({ + const deletedEntities = await EntityService.list({ db, internalCustomerId: customer.internal_id!, inFeatureIds: [feature.internal_id!], isDeleted: true, }); - if (deletedEntities.length == 0) { + if (deletedEntities.length === 0) { logger.info("No deleted entities found"); return; } @@ -112,7 +107,7 @@ const handleInArrearProrated = async ({ for (const linkedCusEnt of cusEnts) { // isLinked - let isLinked = linkedCusEnt.entitlement.entity_feature_id == feature.id; + const isLinked = linkedCusEnt.entitlement.entity_feature_id === feature.id; if (!isLinked) { continue; @@ -123,14 +118,14 @@ const handleInArrearProrated = async ({ ); // Delete cus ent ids - let newEntities = structuredClone(linkedCusEnt.entities!); + const newEntities = structuredClone(linkedCusEnt.entities!); for (const entityId in newEntities) { - if (deletedEntities.some((e) => e.id == entityId)) { + if (deletedEntities.some((e) => e.id === entityId)) { delete newEntities[entityId]; } } - let updated = await CusEntService.update({ + const updated = await CusEntService.update({ db, id: linkedCusEnt.id, updates: { @@ -198,13 +193,13 @@ export const sendUsageAndReset = async ({ for (const cusPrice of cusPrices) { const price = cusPrice.price; - let billingType = getBillingType(price.config); + const billingType = getBillingType(price.config); if (isFixedPrice({ price })) { continue; } - let relatedCusEnt = getRelatedCusEnt({ + const relatedCusEnt = getRelatedCusEnt({ cusPrice, cusEnts, }); @@ -227,19 +222,19 @@ export const sendUsageAndReset = async ({ const subId = invoiceToSubId({ invoice }); - if (!usageBasedSub || usageBasedSub.id != subId) { + if (!usageBasedSub || usageBasedSub.id !== subId) { continue; } // If trial just ended, skip const { start, end } = subToPeriodStartEnd({ sub: usageBasedSub }); - if (usageBasedSub.trial_end == start) { + if (usageBasedSub.trial_end === start) { logger.info(`Trial just ended, skipping usage invoice.created`); continue; } - if (billingType == BillingType.UsageInArrear) { + if (billingType === BillingType.UsageInArrear) { await handleUsagePrices({ db, org, @@ -254,7 +249,7 @@ export const sendUsageAndReset = async ({ }); } - if (billingType == BillingType.InArrearProrated) { + if (billingType === BillingType.InArrearProrated) { await handleContUsePrices({ db, stripeCli, @@ -266,7 +261,7 @@ export const sendUsageAndReset = async ({ }); } - if (billingType == BillingType.UsageInAdvance) { + if (billingType === BillingType.UsageInAdvance) { await handlePrepaidPrices({ db, stripeCli, @@ -324,11 +319,11 @@ export const handleInvoiceCreated = async ({ return; } - let internalEntityId = activeProducts.find( + const internalEntityId = activeProducts.find( (p) => p.internal_entity_id, )?.internal_entity_id; - let features = await FeatureService.list({ + const features = await FeatureService.list({ db, orgId: org.id, env, @@ -372,7 +367,7 @@ export const handleInvoiceCreated = async ({ const stripeSubs = await getStripeSubs({ stripeCli: createStripeCli({ org, env }), - subIds: activeProducts.map((p) => p.subscription_ids || []).flat(), + subIds: activeProducts.flatMap((p) => p.subscription_ids || []), }); for (const activeProduct of activeProducts) { diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts index cdd63c104..ab27e56dc 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts @@ -1,5 +1,6 @@ import { ApiVersion, + AppEnv, type Customer, EntInterval, type FullCusProduct, @@ -20,6 +21,7 @@ import { submitUsageToStripe } from "../../stripeMeterUtils.js"; import { getInvoiceItemForUsage } from "../../stripePriceUtils.js"; import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js"; import { findStripeItemForPrice } from "../../stripeSubUtils/stripeSubItemUtils.js"; +import { getAllFullCustomers } from "@/utils/scriptUtils/getAll/getAllAutumnCustomers.js"; export const handleUsagePrices = async ({ db, @@ -134,6 +136,14 @@ export const handleUsagePrices = async ({ return; } + const allFullCustomers = await getAllFullCustomers({ + db, + orgId: org.id, + env: AppEnv.Live, + }); + + console.log(`All full customers: ${allFullCustomers.length}`); + const ent = relatedCusEnt.entitlement; const resetBalancesUpdate = getResetBalancesUpdate({ diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts index 4d10f96fa..9702b588b 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts @@ -1,13 +1,17 @@ -import { AppEnv, InvoiceStatus } from "@autumn/shared"; -import Stripe from "stripe"; -import { getFullStripeInvoice, invoiceToSubId } from "../stripeInvoiceUtils.js"; -import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtils.js"; -import { MetadataService } from "@/internal/metadata/MetadataService.js"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { + type AppEnv, + type Invoice, + InvoiceStatus, + stripeToAtmnAmount, +} from "@autumn/shared"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { CusService } from "@/internal/customers/CusService.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; +import { MetadataService } from "@/internal/metadata/MetadataService.js"; +import { getFullStripeInvoice, invoiceToSubId } from "../stripeInvoiceUtils.js"; const handleInvoiceCheckoutVoided = async ({ db, @@ -34,7 +38,12 @@ const handleInvoiceCheckoutVoided = async ({ id: metadataId, }); - const { anchorToUnix, config, ...rest } = metadata?.data; + const { + anchorToUnix: _anchorToUnix, + config: _config, + ...rest + } = metadata?.data || {}; + const attachParams = rest as AttachParams; if (!attachParams) return; @@ -51,9 +60,9 @@ const handleInvoiceCheckoutVoided = async ({ if (!subId) return; - const cusSubIds = fullCus.customer_products - .map((cp) => cp.subscription_ids || []) - .flat(); + const cusSubIds = fullCus.customer_products.flatMap( + (cp) => cp.subscription_ids || [], + ); const subIdMatch = cusSubIds.includes(subId); @@ -83,47 +92,46 @@ export const handleInvoiceUpdated = async ({ req: any; }) => { const invoiceObject = event.data.object as Stripe.Invoice; + const currentInvoice = await InvoiceService.getByStripeId({ + db: req.db, + stripeId: invoiceObject.id!, + }); + // const invoice = await getFullStripeInvoice({ // stripeCli, // stripeId: invoiceObject.id!, // }); const prevAttributes = event.data.previous_attributes as any; - const invoiceVoided = - prevAttributes?.status !== "void" && invoiceObject.status === "void"; - const { logger } = req; + const updates: Partial = {}; - if (invoiceVoided) { - logger.info(`Invoice has been voided!`); - - await handleInvoiceCheckoutVoided({ - db: req.db, - stripeCli, - invoiceObject, - logger, - }); - - await InvoiceService.updateByStripeId({ - db: req.db, - stripeId: invoiceObject.id!, - updates: { - status: InvoiceStatus.Void, - }, - }); + if (invoiceObject.status === "void") { + updates.status = InvoiceStatus.Void; } - const invoiceOpen = - prevAttributes?.status !== "open" && invoiceObject.status === "open"; + if (invoiceObject.status === "open") { + updates.status = InvoiceStatus.Open; + } - if (invoiceOpen) { - // logger.info(`Invoice has been opened!`); + if (currentInvoice) { + const newAtmnTotal = stripeToAtmnAmount({ + amount: invoiceObject.total, + currency: invoiceObject.currency, + }); + + const totalEquals = new Decimal(newAtmnTotal).eq(currentInvoice.total); + + if (!totalEquals) { + updates.total = newAtmnTotal; + } + } + + if (Object.keys(updates).length > 0 && invoiceObject.id) { await InvoiceService.updateByStripeId({ db: req.db, - stripeId: invoiceObject.id!, - updates: { - status: InvoiceStatus.Open, - }, + stripeId: invoiceObject.id, + updates, }); } }; diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts index 7e9475064..835b3a346 100644 --- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts +++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts @@ -1,44 +1,36 @@ import { + type AttachConfig, calculateProrationAmount, - Feature, - FeatureOptions, - FullCusProduct, + cusProductToProduct, + type Feature, + type FeatureOptions, + type FullCusProduct, getFeatureInvoiceDescription, OnDecrease, priceToInvoiceAmount, - UsagePriceConfig, + type UsagePriceConfig, } from "@autumn/shared"; - -import { Stripe } from "stripe"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { Decimal } from "decimal.js"; +import type { Stripe } from "stripe"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { featureToCusPrice } from "@/internal/customers/cusProducts/cusPrices/convertCusPriceUtils.js"; +import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js"; +import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; +import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js"; +import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js"; +import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; import { shouldBillNow, shouldProrate, } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js"; - -import { Decimal } from "decimal.js"; -import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; -import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js"; -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 "@autumn/shared"; -import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; -import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js"; import { notNullish } from "@/utils/genUtils.js"; -const onDecreaseToStripeProration: Record = { - [OnDecrease.ProrateImmediately]: "always_invoice", - [OnDecrease.ProrateNextCycle]: "create_prorations", - [OnDecrease.Prorate]: "create_prorations", - [OnDecrease.None]: "none", - [OnDecrease.NoProrations]: "none", -}; - export const handleQuantityDowngrade = async ({ req, attachParams, + attachConfig, cusProduct, stripeSub, oldOptions, @@ -47,13 +39,14 @@ export const handleQuantityDowngrade = async ({ }: { req: any; attachParams: AttachParams; + attachConfig: AttachConfig; cusProduct: FullCusProduct; stripeSub: Stripe.Subscription; oldOptions: FeatureOptions; newOptions: FeatureOptions; subItem: Stripe.SubscriptionItem; }) => { - const { db, logger, org, features } = req; + const { db, logger, org } = req; const { stripeCli, paymentMethod } = attachParams; const cusPrice = featureToCusPrice({ @@ -65,10 +58,6 @@ export const handleQuantityDowngrade = async ({ cusPrice.price.proration_config?.on_decrease || OnDecrease.ProrateImmediately; - const difference = new Decimal(newOptions.quantity) - .minus(oldOptions.quantity) - .toNumber(); - const subItemDifference = new Decimal(newOptions.quantity) .minus( notNullish(oldOptions.upcoming_quantity) @@ -80,18 +69,11 @@ export const handleQuantityDowngrade = async ({ const billingUnits = (cusPrice.price.config as UsagePriceConfig).billing_units || 1; - // const diffWithBillingUnits = new Decimal(difference) - // .mul((cusPrice.price.config as UsagePriceConfig).billing_units || 1) - // .toNumber(); - const newSubItemQuantity = new Decimal(subItem.quantity || 0) .plus(subItemDifference) .toNumber(); - const stripeProration = onDecreaseToStripeProration[ - onDecrease - ] as Stripe.SubscriptionItemUpdateParams.ProrationBehavior; - + let invoice = null; const createDowngradeInvoice = async () => { const { start, end } = subToPeriodStartEnd({ sub: stripeSub }); @@ -117,7 +99,7 @@ export const handleQuantityDowngrade = async ({ const product = cusProductToProduct({ cusProduct }); const feature = req.features.find( - (f: Feature) => f.internal_id == newOptions.internal_feature_id, + (f: Feature) => f.internal_id === newOptions.internal_feature_id, )!; const invoiceItem = constructStripeInvoiceItem({ product, @@ -152,9 +134,12 @@ export const handleQuantityDowngrade = async ({ stripeCusId: stripeSub.customer as string, stripeSubId: stripeSub.id, paymentMethod: paymentMethod || null, + chargeAutomatically: !attachConfig.invoiceOnly, logger, }); + invoice = finalInvoice; + try { const invoiceItems = await getInvoiceItems({ stripeInvoice: finalInvoice, @@ -199,8 +184,7 @@ export const handleQuantityDowngrade = async ({ }); if (cusEnt) { - const config = cusPrice.price.config as UsagePriceConfig; - let decrementBy = new Decimal(oldOptions.quantity) + const decrementBy = new Decimal(oldOptions.quantity) .minus(new Decimal(newOptions.quantity)) .mul(billingUnits) .toNumber(); diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts index 5d602a899..eb861d21a 100644 --- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts +++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts @@ -1,32 +1,30 @@ import { + type AttachConfig, calculateProrationAmount, - Feature, - FeatureOptions, - FullCusProduct, - FullCustomerPrice, - getAmountForQuantity, + cusProductToProduct, + type Feature, + type FeatureOptions, + type FullCusProduct, + type FullCustomerPrice, getFeatureInvoiceDescription, OnIncrease, - UsagePriceConfig, + type UsagePriceConfig, } from "@autumn/shared"; - -import { Stripe } from "stripe"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; - +import { Decimal } from "decimal.js"; +import type { Stripe } from "stripe"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; +import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js"; +import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; +import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js"; +import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js"; +import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; +import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; import { shouldBillNow, shouldProrate, } 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 "@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"; -import { Decimal } from "decimal.js"; -import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; -import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; -import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; import { notNullish } from "@/utils/genUtils.js"; export const handleQuantityUpgrade = async ({ @@ -34,6 +32,7 @@ export const handleQuantityUpgrade = async ({ attachParams, cusProduct, stripeSubs, + attachConfig, oldOptions, newOptions, cusPrice, @@ -43,6 +42,7 @@ export const handleQuantityUpgrade = async ({ req: any; attachParams: AttachParams; cusProduct: FullCusProduct; + attachConfig: AttachConfig; stripeSubs: Stripe.Subscription[]; oldOptions: FeatureOptions; newOptions: FeatureOptions; @@ -74,10 +74,7 @@ export const handleQuantityUpgrade = async ({ const config = cusPrice.price.config as UsagePriceConfig; const billingUnits = config.billing_units || 1; - const diffWithBillingUnits = new Decimal(difference) - .mul((cusPrice.price.config as UsagePriceConfig).billing_units || 1) - .toNumber(); - + let invoice = null; if (prorate && stripeSub?.status !== "trialing") { const { start, end } = subToPeriodStartEnd({ sub: stripeSub }); @@ -101,20 +98,8 @@ export const handleQuantityUpgrade = async ({ }); } - // const amount = priceToInvoiceAmount({ - // price: cusPrice.price, - // quantity: diffWithBillingUnits, - // proration: prorate - // ? { - // start: start * 1000, - // end: end * 1000, - // } - // : undefined, - // now, - // }); - const feature = features.find( - (f: Feature) => f.internal_id == newOptions.internal_feature_id, + (f: Feature) => f.internal_id === newOptions.internal_feature_id, )!; const product = cusProductToProduct({ cusProduct }); @@ -149,6 +134,7 @@ export const handleQuantityUpgrade = async ({ stripeCusId: stripeSub.customer as string, stripeSubId: stripeSub.id, paymentMethod: paymentMethod || null, + chargeAutomatically: !attachConfig.invoiceOnly, logger, }); @@ -173,6 +159,7 @@ export const handleQuantityUpgrade = async ({ } catch (error) { logger.error(`Failed to create invoice from stripe: ${error}`); } + invoice = finalInvoice; } } @@ -184,7 +171,7 @@ export const handleQuantityUpgrade = async ({ // Update cus ent - let cusEnt = getRelatedCusEnt({ + const cusEnt = getRelatedCusEnt({ cusPrice, cusEnts: cusProduct.customer_entitlements, }); @@ -200,4 +187,5 @@ export const handleQuantityUpgrade = async ({ amount: incrementBy, }); } + return { invoice }; }; diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts index 989453b6c..b6859168d 100644 --- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts +++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts @@ -1,23 +1,21 @@ -import { getUsageBasedSub } from "@/external/stripe/stripeSubUtils.js"; -import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; - -import RecaseError from "@/utils/errorUtils.js"; import { + type AttachConfig, ErrCode, - Feature, - FeatureOptions, - FullCusProduct, + type FeatureOptions, + type FullCusProduct, } from "@autumn/shared"; - -import { Stripe } from "stripe"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import type { Stripe } from "stripe"; +import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { featureToCusPrice } from "@/internal/customers/cusProducts/cusPrices/convertCusPriceUtils.js"; -import { handleQuantityUpgrade } from "./handleQuantityUpgrade.js"; +import RecaseError from "@/utils/errorUtils.js"; import { handleQuantityDowngrade } from "./handleQuantityDowngrade.js"; +import { handleQuantityUpgrade } from "./handleQuantityUpgrade.js"; export const handleUpdateFeatureQuantity = async ({ req, attachParams, + attachConfig, cusProduct, stripeSubs, oldOptions, @@ -25,6 +23,7 @@ export const handleUpdateFeatureQuantity = async ({ }: { req: any; attachParams: AttachParams; + attachConfig: AttachConfig; cusProduct: FullCusProduct; stripeSubs: Stripe.Subscription[]; oldOptions: FeatureOptions; @@ -62,7 +61,7 @@ export const handleUpdateFeatureQuantity = async ({ }); } - let subItem = findStripeItemForPrice({ + const subItem = findStripeItemForPrice({ price: price!, stripeItems: subToUpdate.items.data, }) as Stripe.SubscriptionItem; @@ -71,6 +70,7 @@ export const handleUpdateFeatureQuantity = async ({ return await handleQuantityDowngrade({ req, attachParams, + attachConfig, cusProduct, stripeSub: subToUpdate, oldOptions, @@ -81,6 +81,7 @@ export const handleUpdateFeatureQuantity = async ({ return await handleQuantityUpgrade({ req, attachParams, + attachConfig, cusProduct, stripeSubs, oldOptions, @@ -90,34 +91,4 @@ export const handleUpdateFeatureQuantity = async ({ subItem, }); } - - // if (!price) { - // throw new RecaseError({ - // message: `updateFeatureQuantity: No price found for feature ${newOptions.feature_id}`, - // code: ErrCode.PriceNotFound, - // }); - // } - - // if (!subItem) { - // subItem = await stripeCli.subscriptionItems.create({ - // subscription: subToUpdate.id, - // price: price.config.stripe_price_id as string, - // quantity: newOptions.quantity, - // proration_behavior: prorationBehavior, - // payment_behavior: "error_if_incomplete", - // }); - - // logger.info( - // `updateFeatureQuantity: Successfully created sub item for feature ${newOptions.feature_id}: ${newOptions.quantity}`, - // ); - // } else { - // await stripeCli.subscriptionItems.update(subItem.id, { - // quantity: newOptions.quantity, - // proration_behavior: prorationBehavior, - // payment_behavior: "error_if_incomplete", - // }); - // logger.info( - // `updateFeatureQuantity: Successfully updated sub item for feature ${newOptions.feature_id}: ${newOptions.quantity}`, - // ); - // } }; diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateQuantityFlow.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateQuantityFlow.ts index 77105ef1d..3726e98b9 100644 --- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateQuantityFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateQuantityFlow.ts @@ -1,13 +1,13 @@ -import { SuccessCode } from "@autumn/shared"; +import { type AttachConfig, SuccessCode } from "@autumn/shared"; +import type Stripe from "stripe"; +import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { - AttachParams, + type AttachParams, AttachResultSchema, } from "../../../cusProducts/AttachParams.js"; import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js"; import { handleUpdateFeatureQuantity } from "./updateFeatureQuantity.js"; -import { AttachConfig } from "@autumn/shared"; -import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; export const handleUpdateQuantityFunction = async ({ req, @@ -33,15 +33,21 @@ export const handleUpdateQuantityFunction = async ({ subIds: cusProduct.subscription_ids || [], }); + const invoices: Stripe.Invoice[] = []; for (const options of optionsToUpdate) { - await handleUpdateFeatureQuantity({ + const result = await handleUpdateFeatureQuantity({ req, attachParams, + attachConfig: config, cusProduct, stripeSubs, oldOptions: options.old, newOptions: options.new, }); + + if (result?.invoice) { + invoices.push(result.invoice); + } } await CusProductService.update({ @@ -54,6 +60,8 @@ export const handleUpdateQuantityFunction = async ({ AttachResultSchema.parse({ customer_id: customer.id || customer.internal_id, product_ids: attachParams.products.map((p) => p.id), + invoice: + config.invoiceOnly && invoices.length > 0 ? invoices[0] : undefined, code: SuccessCode.FeaturesUpdated, message: `Successfully updated quantity for features: ${optionsToUpdate.map((o) => o.new.feature_id).join(", ")}`, }), diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts index bd71cd1b5..df2d7b7ba 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts @@ -229,6 +229,8 @@ export const handleUpgradeFlow = async ({ } if (res) { + console.log("Invoice:", latestInvoice); + if (req.apiVersion.gte(ApiVersion.V1_1)) { res.status(200).json( AttachResultSchema.parse({ diff --git a/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts index f46315059..ba57c28b5 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts @@ -61,7 +61,7 @@ export const attachParamsToPreview = async ({ let preview: any = null; if ( - branch == AttachBranch.MultiAttach || + branch === AttachBranch.MultiAttach || notNullish(attachParams.productsList) ) { preview = await getMultiAttachPreview({ @@ -73,9 +73,9 @@ export const attachParamsToPreview = async ({ branch, }); } else if ( - func == AttachFunction.AddProduct || - func == AttachFunction.CreateCheckout || - func == AttachFunction.OneOff + func === AttachFunction.AddProduct || + func === AttachFunction.CreateCheckout || + func === AttachFunction.OneOff ) { preview = await getNewProductPreview({ branch, @@ -86,7 +86,7 @@ export const attachParamsToPreview = async ({ }); } - if (func == AttachFunction.ScheduleProduct) { + if (func === AttachFunction.ScheduleProduct) { preview = await getDowngradeProductPreview({ attachParams, now, @@ -98,9 +98,9 @@ export const attachParamsToPreview = async ({ } if ( - func == AttachFunction.UpgradeDiffInterval || - func == AttachFunction.UpgradeSameInterval || - func == AttachFunction.UpdatePrepaidQuantity + func === AttachFunction.UpgradeDiffInterval || + func === AttachFunction.UpgradeSameInterval || + func === AttachFunction.UpdatePrepaidQuantity ) { preview = await getUpgradeProductPreview({ req, diff --git a/server/src/internal/customers/internalCusRouter.ts b/server/src/internal/customers/internalCusRouter.ts index 4b8355c3f..c37616185 100644 --- a/server/src/internal/customers/internalCusRouter.ts +++ b/server/src/internal/customers/internalCusRouter.ts @@ -6,8 +6,7 @@ import { CusExpand, CusProductStatus, ErrCode, - FullCusProduct, - productToCusProduct, + FullCusProduct, productToCusProduct } from "@autumn/shared"; import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js"; @@ -297,32 +296,12 @@ cusRouter.get( }); let productV2 = mapToProductV2({ product: product!, features }); - // let numVersions = await ProductService.getProductVersionCount({ - // db, - // orgId: org.id, - // env, - // productId: product_id, - // }); + + res.status(200).json({ 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({ diff --git a/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts b/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts index 9d7c01768..ddeeb2a18 100644 --- a/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts +++ b/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts @@ -1,5 +1,5 @@ +import type Stripe from "stripe"; import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js"; -import Stripe from "stripe"; export const createAndFinalizeInvoice = async ({ stripeCli, @@ -9,6 +9,7 @@ export const createAndFinalizeInvoice = async ({ invoiceItems, errorOnPaymentFail = true, voidIfFailed = true, + chargeAutomatically = true, logger, }: { stripeCli: Stripe; @@ -18,12 +19,18 @@ export const createAndFinalizeInvoice = async ({ invoiceItems?: Stripe.InvoiceItemCreateParams[]; errorOnPaymentFail?: boolean; voidIfFailed?: boolean; + chargeAutomatically?: boolean; logger?: any; }) => { const invoice = await stripeCli.invoices.create({ customer: stripeCusId, auto_advance: false, subscription: stripeSubId, + + collection_method: chargeAutomatically + ? "charge_automatically" + : "send_invoice", + days_until_due: chargeAutomatically ? undefined : 30, }); if (invoiceItems) { @@ -36,11 +43,19 @@ export const createAndFinalizeInvoice = async ({ } } + if (!chargeAutomatically) { + let finalInvoice = await stripeCli.invoices.retrieve(invoice.id!); + if (finalInvoice.total <= 0) { + finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id!); + } + return { invoice: finalInvoice }; + } + let finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id!, { auto_advance: false, }); - if (finalInvoice.status == "open") { + if (finalInvoice.status === "open") { const { paid, error, diff --git a/server/src/internal/orgs/handlers/handleConnectStripe.ts b/server/src/internal/orgs/handlers/handleConnectStripe.ts index 71d317f3c..5e3a5182d 100644 --- a/server/src/internal/orgs/handlers/handleConnectStripe.ts +++ b/server/src/internal/orgs/handlers/handleConnectStripe.ts @@ -3,7 +3,8 @@ import Stripe from "stripe"; import { z } from "zod"; import { ensureStripeProductsWithEnv } from "@/external/stripe/stripeEnsureUtils.js"; import { - checkKeyValid, createWebhookEndpoint, + checkKeyValid, + createWebhookEndpoint, } from "@/external/stripe/stripeOnboardingUtils.js"; import { createStripeCli } from "@/external/stripe/utils.js"; import { encryptData } from "@/utils/encryptUtils.js"; @@ -291,6 +292,11 @@ export const handleGetStripe = async (req: any, res: any) => { try { const org = await OrgService.getFromReq(req); + if (!isStripeConnected({ org, env: req.env })) { + res.status(200).json({}); + return; + } + const stripeCli = createStripeCli({ org, env: req.env }); const account_details = await stripeCli.accounts.retrieve(); diff --git a/server/src/internal/orgs/orgRouter.ts b/server/src/internal/orgs/orgRouter.ts index 052bc2e9e..f9f478f16 100644 --- a/server/src/internal/orgs/orgRouter.ts +++ b/server/src/internal/orgs/orgRouter.ts @@ -1,21 +1,17 @@ -import express, { Router } from "express"; +import express, { type Router } from "express"; +import { + handleConnectStripe, + handleGetStripe, +} from "./handlers/handleConnectStripe.js"; +import { handleDeleteOrg } from "./handlers/handleDeleteOrg.js"; +import { handleDeleteStripe } from "./handlers/handleDeleteStripe.js"; +import { handleGetInvites } from "./handlers/handleGetInvites.js"; +import { handleGetOrg } from "./handlers/handleGetOrg.js"; import { handleGetOrgMembers, handleRemoveMember, } from "./handlers/handleGetOrgMembers.js"; - -import { OrgService } from "./OrgService.js"; -import { handleRequestError } from "@/utils/errorUtils.js"; -import { createOrgResponse } from "./orgUtils.js"; import { handleGetUploadUrl } from "./handlers/handleGetUploadUrl.js"; -import { handleDeleteOrg } from "./handlers/handleDeleteOrg.js"; -import { handleGetInvites } from "./handlers/handleGetInvites.js"; -import { - handleConnectStripe, - handleGetStripe, -} from "./handlers/handleConnectStripe.js"; -import { handleDeleteStripe } from "./handlers/handleDeleteStripe.js"; -import { handleGetOrg } from "./handlers/handleGetOrg.js"; export const orgRouter: Router = express.Router(); orgRouter.get("/members", handleGetOrgMembers); diff --git a/vite/biome.json b/vite/biome.json index 9ab8d16be..57cb52103 100644 --- a/vite/biome.json +++ b/vite/biome.json @@ -17,6 +17,9 @@ "linter": { "enabled": true, "rules": { + "correctness": { + "useExhaustiveDependencies": "off" + }, "recommended": true, "complexity": { "noStaticOnlyClass": "off" diff --git a/vite/package.json b/vite/package.json index c0daf0f62..f14460e9d 100644 --- a/vite/package.json +++ b/vite/package.json @@ -18,7 +18,6 @@ "dependencies": { "@autumn/shared": "workspace:*", "@better-auth/stripe": "^1.2.12", - "@clerk/clerk-react": "^5.24.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@heroicons/react": "^2.2.0", diff --git a/vite/src/App.tsx b/vite/src/App.tsx index e2512d756..6c7e6768d 100644 --- a/vite/src/App.tsx +++ b/vite/src/App.tsx @@ -1,23 +1,21 @@ -import OnboardingView2 from "./views/onboarding2/OnboardingView2"; +import { AppEnv } from "@autumn/shared"; +import { BrowserRouter, Route, Routes } from "react-router"; +import { MainLayout } from "./app/layout"; +import { AdminView } from "./views/admin/AdminView"; +import { AcceptInvitation } from "./views/auth/AcceptInvitation"; +import { PasswordSignIn } from "./views/auth/components/PasswordSignIn"; +import { SignIn } from "./views/auth/SignIn"; +import { Otp } from "./views/cli/Otp"; +import CustomersPage from "./views/customers/CustomersPage"; +import { AnalyticsView } from "./views/customers/customer/analytics/AnalyticsView"; import CustomerView from "./views/customers/customer/CustomerView"; import CustomerProductView from "./views/customers/customer/product/CustomerProductView"; -import DevScreen from "./views/developer/DevView"; -import ProductView from "./views/products/product/ProductView"; -import ProductsView from "./views/products/ProductsView"; -import CliAuth from "./views/CliAuth"; - -import { AppEnv } from "@autumn/shared"; -import { Otp } from "./views/cli/Otp"; -import { SignIn } from "./views/auth/SignIn"; -import { AcceptInvitation } from "./views/auth/AcceptInvitation"; -import { BrowserRouter, Routes, Route } from "react-router"; -import { AdminView } from "./views/admin/AdminView"; -import { PasswordSignIn } from "./views/auth/components/PasswordSignIn"; -import { AnalyticsView } from "./views/customers/customer/analytics/AnalyticsView"; -import { TerminalView } from "./views/TerminalView"; import { DefaultView } from "./views/DefaultView"; -import { MainLayout } from "./app/layout"; -import CustomersPage from "./views/customers/CustomersPage"; +import DevScreen from "./views/developer/DevView"; +import OnboardingView2 from "./views/onboarding2/OnboardingView2"; +import ProductsView from "./views/products/ProductsView"; +import ProductView from "./views/products/product/ProductView"; +import { TerminalView } from "./views/TerminalView"; export default function App() { return ( @@ -32,7 +30,6 @@ export default function App() { } /> } /> } /> - } /> { const axiosInstance = useAxiosInstance(); diff --git a/vite/src/hooks/queries/useOrgStripeQuery.tsx b/vite/src/hooks/queries/useOrgStripeQuery.tsx new file mode 100644 index 000000000..a0ebea4d4 --- /dev/null +++ b/vite/src/hooks/queries/useOrgStripeQuery.tsx @@ -0,0 +1,32 @@ +import { useQuery } from "@tanstack/react-query"; +import type Stripe from "stripe"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useOrg } from "../common/useOrg"; + +/** Fetches organization Stripe account information */ +export const useOrgStripeQuery = () => { + const axiosInstance = useAxiosInstance(); + // const orgId = authClient.getSession()?.session.activeOrganizationId; + const { org } = useOrg(); + + const fetchStripeAccount = async () => { + const { data } = await axiosInstance.get( + "/organization/stripe", + ); + return data; + }; + + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ["org", org?.id, "stripe"], + queryFn: fetchStripeAccount, + retry: false, + enabled: !!org?.id, + }); + + return { + stripeAccount: data || null, + isLoading, + error, + refetch, + }; +}; diff --git a/vite/src/utils/linkUtils.ts b/vite/src/utils/linkUtils.ts index 577e87310..e6591f259 100644 --- a/vite/src/utils/linkUtils.ts +++ b/vite/src/utils/linkUtils.ts @@ -1,36 +1,67 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: can use any*/ + import { AppEnv } from "@autumn/shared"; -export const getStripeCusLink = ( - customerId: string, - env: AppEnv, - accountId?: string -) => { - return `https://dashboard.stripe.com${ - env == AppEnv.Live ? "" : "/test" - }/${accountId}/customers/${customerId}`; +export const getStripeCusLink = ({ + customerId, + env, + accountId, +}: { + customerId: string; + env: AppEnv; + accountId?: string; +}) => { + const baseUrl = `https://dashboard.stripe.com${ + env === AppEnv.Live ? "" : "/test" + }`; + const accountPath = accountId ? `/${accountId}` : ""; + return `${baseUrl}${accountPath}/customers/${customerId}`; }; -export const getStripeSubLink = ( - subscriptionId: string, - env: AppEnv, - accountId?: string -) => { - return `https://dashboard.stripe.com${ - env == AppEnv.Live ? "" : "/test" - }/${accountId}/subscriptions/${subscriptionId}`; -}; -export const getStripeSubScheduleLink = ( - scheduledId: string, - env: AppEnv, - accountId?: string -) => { - return `https://dashboard.stripe.com${ - env == AppEnv.Live ? "" : "/test" - }/${accountId}/subscription_schedules/${scheduledId}`; +export const getStripeSubLink = ({ + subscriptionId, + env, + accountId, +}: { + subscriptionId: string; + env: AppEnv; + accountId?: string; +}) => { + const baseUrl = `https://dashboard.stripe.com${ + env === AppEnv.Live ? "" : "/test" + }`; + const accountPath = accountId ? `/${accountId}` : ""; + return `${baseUrl}${accountPath}/subscriptions/${subscriptionId}`; }; -export const getStripeInvoiceLink = (stripeInvoice: any) => { - return `https://dashboard.stripe.com${ - stripeInvoice.livemode ? "" : "/test" - }/invoices/${stripeInvoice.id || stripeInvoice.stripe_id}`; +export const getStripeSubScheduleLink = ({ + scheduledId, + env, + accountId, +}: { + scheduledId: string; + env: AppEnv; + accountId?: string; +}) => { + const baseUrl = `https://dashboard.stripe.com${ + env === AppEnv.Live ? "" : "/test" + }`; + const accountPath = accountId ? `/${accountId}` : ""; + return `${baseUrl}${accountPath}/subscription_schedules/${scheduledId}`; +}; + +export const getStripeInvoiceLink = ({ + stripeInvoice, + env, + accountId, +}: { + stripeInvoice: any; + env: AppEnv; + accountId?: string; +}) => { + const baseUrl = `https://dashboard.stripe.com${ + env === AppEnv.Live ? "" : "/test" + }`; + const accountPath = accountId ? `/${accountId}` : ""; + return `${baseUrl}${accountPath}/invoices/${stripeInvoice.id || stripeInvoice.stripe_id}`; }; diff --git a/vite/src/views/CliAuth.tsx b/vite/src/views/CliAuth.tsx deleted file mode 100644 index 3fea96bb0..000000000 --- a/vite/src/views/CliAuth.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useEffect, useState } from "react"; -import { useSearchParams } from "react-router"; -import axios from "axios"; -import { useSession } from "@clerk/clerk-react"; -import LoadingScreen from "./general/LoadingScreen"; -import ErrorScreen from "./general/ErrorScreen"; -export default function CliAuth() { - let [searchParams] = useSearchParams(); - let { session } = useSession(); - - let [savedToken, setSavedToken] = useState(false); - let [error, setError] = useState(false); - - const handleCallback = async () => { - let code = searchParams.get("code"); - let redirectUrl = searchParams.get("redirect"); - - if (!redirectUrl) { - return; - } - - let token = await session?.getToken({ - template: "cli_template", - }); - - try { - await axios.get(redirectUrl, { - params: { - token: token, - }, - }); - setSavedToken(true); - } catch (error) { - setError(true); - } - }; - - useEffect(() => { - if (!session) { - return; - } - - handleCallback(); - }, [searchParams, session]); - - if (savedToken) { - return ( -
-
āœ… Successfully authenticated CLI
-
- ); - } - - if (error) { - return Something went wrong, please try again; - } - - return ; -} diff --git a/vite/src/views/customers/customer/InvoicesTable.tsx b/vite/src/views/customers/customer/InvoicesTable.tsx index 0fe24871a..7c154f469 100644 --- a/vite/src/views/customers/customer/InvoicesTable.tsx +++ b/vite/src/views/customers/customer/InvoicesTable.tsx @@ -1,25 +1,34 @@ -import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils"; -import { useCustomerContext } from "./CustomerContext"; -import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { Invoice, Product } from "@autumn/shared"; +import { + type Entity, + type Invoice, + type InvoiceDiscount, + Product, +} from "@autumn/shared"; import { toast } from "sonner"; -import { getStripeInvoiceLink } from "@/utils/linkUtils"; -import { Row, Item } from "@/components/general/TableGrid"; import { AdminHover } from "@/components/general/AdminHover"; +import { Item, Row } from "@/components/general/TableGrid"; +import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; import { cn } from "@/lib/utils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useEnv } from "@/utils/envUtils"; +import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils"; +import { getStripeInvoiceLink } from "@/utils/linkUtils"; +import { useCustomerContext } from "./CustomerContext"; import { CusProductEntityItem } from "./components/CusProductEntityItem"; import { useCusQuery } from "./hooks/useCusQuery"; export const InvoicesTable = () => { // const { env, invoices, products, entityId, entities, showEntityView } = // useCustomerContext(); + const env = useEnv(); const { entityId, showEntityView } = useCustomerContext(); const { customer, products, entities } = useCusQuery(); + const { stripeAccount } = useOrgStripeQuery(); const axiosInstance = useAxiosInstance(); const invoices = customer.invoices; const entity = entities.find( - (e: any) => e.id === entityId || e.internal_id === entityId, + (e: Entity) => e.id === entityId || e.internal_id === entityId, ); const getStripeInvoice = async (stripeInvoiceId: string) => { @@ -28,16 +37,19 @@ export const InvoicesTable = () => { `/v1/invoices/${stripeInvoiceId}/stripe`, ); return data; - } catch (error) { + } catch { toast.error("Failed to get invoice URL"); return null; } }; const getTotalDiscountAmount = (invoice: Invoice) => { - return invoice.discounts.reduce((acc: number, discount: any) => { - return acc + discount.amount_used; - }, 0); + return invoice.discounts.reduce( + (acc: number, discount: InvoiceDiscount) => { + return acc + discount.amount_used; + }, + 0, + ); }; const invoicesFiltered = invoices.filter((invoice: Invoice) => { @@ -60,22 +72,17 @@ export const InvoicesTable = () => {

No invoice history found

) : ( - <> - - Products - {showEntityView && Entity} - Total - Status - Created At - - - + + Products + {showEntityView && Entity} + Total + Status + Created At + + )} {invoicesFiltered.map((invoice: Invoice) => ( @@ -85,12 +92,18 @@ export const InvoicesTable = () => { onClick={async () => { const stripeInvoice = await getStripeInvoice(invoice.stripe_id); if (!stripeInvoice.hosted_invoice_url) { - const livemode = stripeInvoice.livemode; - window.open(getStripeInvoiceLink(stripeInvoice), "_blank"); + window.open( + getStripeInvoiceLink({ + stripeInvoice, + env, + accountId: stripeAccount?.id, + }), + "_blank", + ); return; } - if (stripeInvoice && stripeInvoice.hosted_invoice_url) { + if (stripeInvoice?.hosted_invoice_url) { window.open(stripeInvoice.hosted_invoice_url, "_blank"); } }} diff --git a/vite/src/views/customers/customer/components/customer-sidebar/CustomerDetails.tsx b/vite/src/views/customers/customer/components/customer-sidebar/CustomerDetails.tsx index df1cca754..c2d3c0b0c 100644 --- a/vite/src/views/customers/customer/components/customer-sidebar/CustomerDetails.tsx +++ b/vite/src/views/customers/customer/components/customer-sidebar/CustomerDetails.tsx @@ -1,167 +1,145 @@ -import CopyButton from "@/components/general/CopyButton"; -import { SideAccordion } from "@/components/general/SideAccordion"; -import { getStripeCusLink } from "@/utils/linkUtils"; import { faStripe } from "@fortawesome/free-brands-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ArrowUpRightFromSquare } from "lucide-react"; - -import { Button } from "@/components/ui/button"; import { Link } from "react-router"; -import { useEnv } from "@/utils/envUtils"; -import { SidebarLabel } from "@/components/general/sidebar/sidebar-label"; -import { useCusQuery } from "../../hooks/useCusQuery"; -import { useOrg } from "@/hooks/common/useOrg"; import Stripe from "stripe"; -import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { toast } from "sonner"; +import CopyButton from "@/components/general/CopyButton"; +import { SideAccordion } from "@/components/general/SideAccordion"; +import { SidebarLabel } from "@/components/general/sidebar/sidebar-label"; +import { Button } from "@/components/ui/button"; +import { useOrg } from "@/hooks/common/useOrg"; +import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; +import { useEnv } from "@/utils/envUtils"; +import { getStripeCusLink } from "@/utils/linkUtils"; +import { useCusQuery } from "../../hooks/useCusQuery"; export const CustomerDetails = ({ - setIsModalOpen, - setModalType, + setIsModalOpen, + setModalType, }: { - setIsModalOpen: (isModalOpen: boolean) => void; - setModalType: (modalType: string) => void; + setIsModalOpen: (isModalOpen: boolean) => void; + setModalType: (modalType: string) => void; }) => { - const { customer } = useCusQuery(); - const env = useEnv(); - const { org } = useOrg(); + const { customer } = useCusQuery(); + const env = useEnv(); + const { org } = useOrg(); + const { stripeAccount } = useOrgStripeQuery(); - const axiosInstance = useAxiosInstance(); + return ( +
+ +
+ ID +
+
+ {customer.id ? ( + + {customer.id} + + ) : ( + + )} +
+
- const getStripeAccountInfo = async () => { - try { - const { data } = await axiosInstance.get(`/organization/stripe`); - return data; - } catch (error) { - toast.error("Failed to get invoice URL"); - return null; - } - }; + Name +
+ +
- return ( -
- -
- ID -
-
- {customer.id ? ( - - {customer.id} - - ) : ( - - )} -
-
+ Email +
+ +
- Name -
- -
+ + Fingerprint + +
+ +
- Email -
- -
- - - Fingerprint - -
- -
- - {customer.processor?.id && ( - <> - - Stripe - -
-
-
- -
-
-
- - )} -
-
-
- ); + {customer.processor?.id && ( + <> + + Stripe + +
+
+
+ +
+
+
+ + )} +
+
+
+ ); }; diff --git a/vite/src/views/customers/customer/customer-product-list/CusProductStripeLink.tsx b/vite/src/views/customers/customer/customer-product-list/CusProductStripeLink.tsx index ec2a18ddd..843b4f3dd 100644 --- a/vite/src/views/customers/customer/customer-product-list/CusProductStripeLink.tsx +++ b/vite/src/views/customers/customer/customer-product-list/CusProductStripeLink.tsx @@ -1,98 +1,95 @@ -import { useOrg } from "@/hooks/common/useOrg"; -import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { useEnv } from "@/utils/envUtils"; -import { getStripeSubLink, getStripeSubScheduleLink } from "@/utils/linkUtils"; -import { CusProductStatus, FullCusProduct } from "@autumn/shared"; +import { CusProductStatus, type FullCusProduct } from "@autumn/shared"; import { ArrowUpRightFromSquare } from "lucide-react"; import React from "react"; import { Link } from "react-router"; -import { toast } from "sonner"; +import { useOrg } from "@/hooks/common/useOrg"; +import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; +import { useEnv } from "@/utils/envUtils"; +import { getStripeSubLink, getStripeSubScheduleLink } from "@/utils/linkUtils"; export const CusProductStripeLink = ({ - cusProduct, + cusProduct, }: { - cusProduct: FullCusProduct; + cusProduct: FullCusProduct; }) => { - const env = useEnv(); - - const { org } = useOrg(); - - const axiosInstance = useAxiosInstance(); - - const getStripeAccountInfo = async () => { - try { - const { data } = await axiosInstance.get(`/organization/stripe`); - return data; - } catch (error) { - toast.error("Failed to get invoice URL"); - return null; - } - }; - return ( - <> - {cusProduct.subscription_ids && - cusProduct.subscription_ids.length > 0 && ( - - {cusProduct.subscription_ids.map((subId: string) => { - return ( -
{ - e.stopPropagation(); - const account = await getStripeAccountInfo(); - if (account) { - window.open( - getStripeSubLink(subId, env, account.id), - "_blank" - ); - } else { - window.open(getStripeSubLink(subId, env)); - } - }} - > -
- -
-
- ); - })} -
- )} - {cusProduct.status == CusProductStatus.Scheduled && - cusProduct.scheduled_ids && - cusProduct.scheduled_ids.length > 0 && ( - - {cusProduct.scheduled_ids.map((subId: string) => { - return ( -
{ - e.stopPropagation(); - const account = await getStripeAccountInfo(); - if (account) { - window.open( - getStripeSubScheduleLink(subId, env, account.id), - "_blank" - ); - } else { - window.open(getStripeSubScheduleLink(subId, env)); - } - }} - > -
- -
-
- ); - })} -
- )} - - ); + const env = useEnv(); + const { org } = useOrg(); + const { stripeAccount } = useOrgStripeQuery(); + return ( + <> + {cusProduct.subscription_ids && + cusProduct.subscription_ids.length > 0 && ( + + {cusProduct.subscription_ids.map((subId: string) => { + return ( +
{ + e.stopPropagation(); + if (stripeAccount) { + window.open( + getStripeSubLink({ + subscriptionId: subId, + env, + accountId: stripeAccount.id, + }), + "_blank", + ); + } else { + window.open( + getStripeSubLink({ subscriptionId: subId, env }), + ); + } + }} + > +
+ +
+
+ ); + })} +
+ )} + {cusProduct.status == CusProductStatus.Scheduled && + cusProduct.scheduled_ids && + cusProduct.scheduled_ids.length > 0 && ( + + {cusProduct.scheduled_ids.map((subId: string) => { + return ( +
{ + e.stopPropagation(); + if (stripeAccount) { + window.open( + getStripeSubScheduleLink({ + scheduledId: subId, + env, + accountId: stripeAccount.id, + }), + "_blank", + ); + } else { + window.open( + getStripeSubScheduleLink({ scheduledId: subId, env }), + ); + } + }} + > +
+ +
+
+ ); + })} +
+ )} + + ); }; diff --git a/vite/src/views/customers/customer/product/CustomerProductView.tsx b/vite/src/views/customers/customer/product/CustomerProductView.tsx index 04cfa6dd0..0d376fee6 100644 --- a/vite/src/views/customers/customer/product/CustomerProductView.tsx +++ b/vite/src/views/customers/customer/product/CustomerProductView.tsx @@ -1,12 +1,11 @@ "use client"; -import { type ProductItem, type ProductV2 } from "@autumn/shared"; +import type { ProductItem, ProductV2 } from "@autumn/shared"; import { useEffect, useRef, useState } from "react"; import { Link, useParams, useSearchParams } from "react-router"; import { CustomToaster } from "@/components/general/CustomToaster"; import { useOrg } from "@/hooks/common/useOrg"; import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; -import { useEnv } from "@/utils/envUtils"; import { notNullish } from "@/utils/genUtils"; import { sortProductItems } from "@/utils/productUtils"; import ErrorScreen from "@/views/general/ErrorScreen"; @@ -52,7 +51,6 @@ export default function CustomerProductView() { const { isLoading: orgLoading } = useOrg(); const { isLoading: featuresLoading } = useFeaturesQuery(); - const env = useEnv(); const initialProductRef = useRef(null); const [options, setOptions] = useState([]); @@ -60,7 +58,6 @@ export default function CustomerProductView() { const [entityFeatureIds, setEntityFeatureIds] = useState([]); const version = searchParams.get("version"); - const customer_product_id = searchParams.get("id"); const { product: originalProduct, @@ -94,12 +91,17 @@ export default function CustomerProductView() { const product = originalProduct; - setProduct(product); + console.log('[CPV] effect', { prodId: originalProduct.id, v: originalProduct.version, cusId: cusProduct?.id }); + + // Update initialProductRef BEFORE setProduct to ensure useAttachState + // effect has the correct baseline when it runs initialProductRef.current = structuredClone({ ...product, items: sortProductItems(product.items), }); + setProduct(product); + setEntityFeatureIds( Array.from( new Set( diff --git a/vite/src/views/customers/customer/product/components/AttachModal.tsx b/vite/src/views/customers/customer/product/components/AttachModal.tsx index 8f61a0fd5..6fa829c7c 100644 --- a/vite/src/views/customers/customer/product/components/AttachModal.tsx +++ b/vite/src/views/customers/customer/product/components/AttachModal.tsx @@ -17,6 +17,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; import { cn } from "@/lib/utils"; import { CusService } from "@/services/customers/CusService"; import { useAxiosInstance } from "@/services/useAxiosInstance"; @@ -45,6 +46,7 @@ export const AttachModal = ({ }) => { const { customer, entities } = useCusQuery(); const { product, entityId, attachState, version } = useProductContext(); + const { stripeAccount } = useOrgStripeQuery(); const navigation = useNavigate(); const env = useEnv(); @@ -169,7 +171,14 @@ export const AttachModal = ({ if (data.checkout_url) { window.open(data.checkout_url, "_blank"); } else if (data.invoice) { - window.open(getStripeInvoiceLink(data.invoice), "_blank"); + window.open( + getStripeInvoiceLink({ + stripeInvoice: data.invoice, + env, + accountId: stripeAccount?.id, + }), + "_blank", + ); } navigateTo(`/customers/${cusId}`, navigation, env); diff --git a/vite/src/views/customers/customer/product/hooks/useAttachState.tsx b/vite/src/views/customers/customer/product/hooks/useAttachState.tsx index 6cade12bf..84fdc9044 100644 --- a/vite/src/views/customers/customer/product/hooks/useAttachState.tsx +++ b/vite/src/views/customers/customer/product/hooks/useAttachState.tsx @@ -119,6 +119,8 @@ export const useAttachState = ({ free_trial: initialProductRef.current?.free_trial || null, }); + console.log('[UAS] effect', { changed: hasItemsChanged, refLen: initialProductRef.current?.items?.length, hasPrepaid: productHasPrepaid(product.items) }); + setItemsChanged(hasItemsChanged); }, [product]); @@ -163,21 +165,26 @@ export const useAttachState = ({ }; const getButtonText = () => { - if (cusProduct && !itemsChanged) { - if (flags.isOneOff) { - return "Attach Product"; + const result = (() => { + if (cusProduct && !itemsChanged) { + if (flags.isOneOff) { + return "Attach Product"; + } + + if (flags.hasPrepaid) { + return "Update prepaid quantity"; + } } - if (flags.hasPrepaid) { - return "Update prepaid quantity"; + if (flags.isCanceled) { + return "Renew Product"; } - } - if (flags.isCanceled) { - return "Renew Product"; - } + return "Attach Product"; + })(); - return "Attach Product"; + console.log('[BTN]', { text: result, cusId: cusProduct?.id, changed: itemsChanged, prepaid: flags.hasPrepaid }); + return result; }; return { diff --git a/vite/src/views/customers/customer/product/hooks/useCusProductCache.tsx b/vite/src/views/customers/customer/product/hooks/useCusProductCache.tsx index f3fe988ec..ac84d73bf 100644 --- a/vite/src/views/customers/customer/product/hooks/useCusProductCache.tsx +++ b/vite/src/views/customers/customer/product/hooks/useCusProductCache.tsx @@ -1,14 +1,14 @@ -import { useQueryClient } from "@tanstack/react-query"; import { ACTIVE_STATUSES, cusProductToProduct, - FullCusProduct, - FullCustomer, + type FullCusProduct, + type FullCustomer, mapToProductV2, notNullish, + type ProductV2, productToCusProduct, - ProductV2, } from "@autumn/shared"; +import { useQueryClient } from "@tanstack/react-query"; export const useCusProductCache = ({ customerId, @@ -81,15 +81,12 @@ export const useCusProductCache = ({ : cachedData.customer.entity?.internal_id; const cusProduct = productToCusProduct({ - productId: productId!, + productId, cusProducts, internalEntityId, version: queryStates.version, cusProductId: queryStates.customerProductId, inStatuses: ACTIVE_STATUSES, - // version: undefined, - // cusProductId: undefined, - // inStatuses: ACTIVE_STATUSES, }); if (cusProduct) { diff --git a/vite/src/views/customers/customer/product/hooks/useCusProductQuery.tsx b/vite/src/views/customers/customer/product/hooks/useCusProductQuery.tsx index 56339e63b..8f88f9c67 100644 --- a/vite/src/views/customers/customer/product/hooks/useCusProductQuery.tsx +++ b/vite/src/views/customers/customer/product/hooks/useCusProductQuery.tsx @@ -1,11 +1,11 @@ -import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { FullCusProduct, ProductV2 } from "@autumn/shared"; +import type { FullCusProduct, ProductV2 } from "@autumn/shared"; import { useQuery } from "@tanstack/react-query"; -import { parseAsInteger, parseAsString, useQueryStates } from "nuqs"; -import { useParams } from "react-router"; -import { useCusProductCache } from "./useCusProductCache"; -import { useEffect, useMemo, useState } from "react"; import { debounce } from "lodash"; +import { parseAsInteger, parseAsString, useQueryStates } from "nuqs"; +import { useEffect, useMemo, useState } from "react"; +import { useParams } from "react-router"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useCusProductCache } from "./useCusProductCache"; export const useCusProductQuery = () => { const axiosInstance = useAxiosInstance(); @@ -28,7 +28,7 @@ export const useCusProductQuery = () => { }, }); - const cachedCusProduct = useMemo(getCachedCusProduct, [getCachedCusProduct]); + const cachedCusProduct = useMemo(getCachedCusProduct, []); const fetcher = async () => { const queryParams = { diff --git a/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx b/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx index dccac0e5a..ab10f1412 100644 --- a/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx +++ b/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx @@ -1,14 +1,17 @@ +import type { ProductV2 } from "@autumn/shared"; +import type { CheckoutResult } from "autumn-js"; +import { ArrowUpRightFromSquare, Loader2, Plus, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; import { CustomDialogBody, CustomDialogContent, CustomDialogFooter, } from "@/components/general/modal-components/DialogContentWrapper"; +import FieldLabel from "@/components/general/modal-components/FieldLabel"; import { Button } from "@/components/ui/button"; import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { InvoiceCustomerButton } from "../components/InvoiceCustomerButton"; -import FieldLabel from "@/components/general/modal-components/FieldLabel"; -import { ArrowUpRightFromSquare, Loader2, Plus, X } from "lucide-react"; - +import { Input } from "@/components/ui/input"; import { Select, SelectContent, @@ -16,22 +19,20 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useEffect, useState } from "react"; -import { ProductV2 } from "@autumn/shared"; -import { Input } from "@/components/ui/input"; -import { toast } from "sonner"; +import { useOrg } from "@/hooks/common/useOrg"; +import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useAxiosSWR } from "@/services/useAxiosSwr"; +import { useEnv } from "@/utils/envUtils"; +import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils"; import { getBackendErr } from "@/utils/genUtils"; -import { MultiAtttachLines } from "./MultiAttachLines"; -import { CheckoutResult } from "autumn-js"; import { getStripeInvoiceLink } from "@/utils/linkUtils"; import { formatAmount } from "@/utils/product/productItemUtils"; -import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils"; -import { AddRewardButton, MultiAttachRewards } from "./MultiAttachRewards"; -import { useAxiosSWR } from "@/services/useAxiosSwr"; import { useCusQuery } from "../../hooks/useCusQuery"; -import { useOrg } from "@/hooks/common/useOrg"; +import { InvoiceCustomerButton } from "../components/InvoiceCustomerButton"; import { getCusProductMinQuantity } from "../utils/getCusProductMinQuantity"; +import { MultiAtttachLines } from "./MultiAttachLines"; +import { AddRewardButton, MultiAttachRewards } from "./MultiAttachRewards"; export const MultiAttachDialog = ({ open, @@ -43,6 +44,8 @@ export const MultiAttachDialog = ({ // const { customer, cusMutate, products, org } = useCustomerContext(); const { org } = useOrg(); const { customer, products, refetch } = useCusQuery(); + const { stripeAccount } = useOrgStripeQuery(); + const env = useEnv(); const axiosInstance = useAxiosInstance(); @@ -157,7 +160,14 @@ export const MultiAttachDialog = ({ }); if (data.invoice) { - window.open(getStripeInvoiceLink(data.invoice), "_blank"); + window.open( + getStripeInvoiceLink({ + stripeInvoice: data.invoice, + env, + accountId: stripeAccount?.id, + }), + "_blank", + ); } await refetch(); diff --git a/vite/vite.config.ts b/vite/vite.config.ts index 1305fbe0a..a0b0bffbc 100644 --- a/vite/vite.config.ts +++ b/vite/vite.config.ts @@ -13,10 +13,19 @@ export default defineConfig({ }, optimizeDeps: { // Exclude workspace dependencies from pre-bundling to avoid cache issues - exclude: ["@autumn/shared"], + exclude: [ + "@autumn/shared", + "better-auth", + "better-auth/react", + "@better-auth/stripe", + ], // Force re-optimization on server start to catch workspace changes force: process.env.FORCE_OPTIMIZE === "true", + // Include specific dependencies that might cause issues + include: [], }, + // Clear cache on config change + cacheDir: "node_modules/.vite", server: { host: "0.0.0.0", // Required for Docker port: process.env.VITE_PORT