From 98c5e36103936ed0111d5132fd7e50fd0e8594bd Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Tue, 23 Sep 2025 04:55:15 +0530 Subject: [PATCH 1/3] changes for introducing the backend logic to get the accountid from the stripe client --- .../orgs/handlers/handleConnectStripe.ts | 479 +++++++++--------- server/src/internal/orgs/orgRouter.ts | 7 +- 2 files changed, 253 insertions(+), 233 deletions(-) diff --git a/server/src/internal/orgs/handlers/handleConnectStripe.ts b/server/src/internal/orgs/handlers/handleConnectStripe.ts index f5da09fba..2471b9cde 100644 --- a/server/src/internal/orgs/handlers/handleConnectStripe.ts +++ b/server/src/internal/orgs/handlers/handleConnectStripe.ts @@ -1,13 +1,13 @@ import { routeHandler } from "@/utils/routerUtils.js"; import Stripe from "stripe"; -import RecaseError from "@/utils/errorUtils.js"; +import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; import { encryptData } from "@/utils/encryptUtils.js"; import { ErrCode } from "@/errors/errCodes.js"; import { - checkKeyValid, - createWebhookEndpoint, + checkKeyValid, + createWebhookEndpoint, } from "@/external/stripe/stripeOnboardingUtils.js"; import { OrgService } from "../OrgService.js"; @@ -17,280 +17,295 @@ import { clearOrgCache } from "../orgUtils/clearOrgCache.js"; import { z } from "zod"; import { isStripeConnected } from "../orgUtils.js"; import { - ensureStripeProducts, - ensureStripeProductsWithEnv, + ensureStripeProducts, + ensureStripeProductsWithEnv, } from "@/external/stripe/stripeEnsureUtils.js"; import { toSuccessUrl } from "../orgUtils/convertOrgUtils.js"; +import { createStripeCli } from "@/external/stripe/utils.js"; export const connectStripe = async ({ - orgId, - apiKey, - env, + orgId, + apiKey, + env, }: { - orgId: string; - apiKey: string; - env: AppEnv; + orgId: string; + apiKey: string; + env: AppEnv; }) => { - // 1. Check if key is valid - await checkKeyValid(apiKey); + // 1. Check if key is valid + await checkKeyValid(apiKey); - let stripe = new Stripe(apiKey); + let stripe = new Stripe(apiKey); - let account = await stripe.accounts.retrieve(); + let account = await stripe.accounts.retrieve(); - // 2. Disconnect existing webhook endpoints - const curWebhooks = await stripe.webhookEndpoints.list(); - for (const webhook of curWebhooks.data) { - if (webhook.url.includes(orgId) && webhook.url.includes(env)) { - await stripe.webhookEndpoints.del(webhook.id); - } - } + // 2. Disconnect existing webhook endpoints + const curWebhooks = await stripe.webhookEndpoints.list(); + for (const webhook of curWebhooks.data) { + if (webhook.url.includes(orgId) && webhook.url.includes(env)) { + await stripe.webhookEndpoints.del(webhook.id); + } + } - // 3. Create new webhook endpoint - let webhook = await createWebhookEndpoint(apiKey, env, orgId); + // 3. Create new webhook endpoint + let webhook = await createWebhookEndpoint(apiKey, env, orgId); - // 3. Return encrypted - if (env === AppEnv.Sandbox) { - return { - test_api_key: encryptData(apiKey), - test_webhook_secret: encryptData(webhook.secret as string), - env, - defaultCurrency: account.default_currency, - }; - } else { - return { - live_api_key: encryptData(apiKey), - live_webhook_secret: encryptData(webhook.secret as string), - env, - defaultCurrency: account.default_currency, - }; - } + // 3. Return encrypted + if (env === AppEnv.Sandbox) { + return { + test_api_key: encryptData(apiKey), + test_webhook_secret: encryptData(webhook.secret as string), + env, + defaultCurrency: account.default_currency, + }; + } else { + return { + live_api_key: encryptData(apiKey), + live_webhook_secret: encryptData(webhook.secret as string), + env, + defaultCurrency: account.default_currency, + }; + } }; export const connectAllStripe = async ({ - db, - orgId, - logger, - testApiKey, - liveApiKey, - defaultCurrency, - successUrl, + db, + orgId, + logger, + testApiKey, + liveApiKey, + defaultCurrency, + successUrl, }: { - db: any; - orgId: string; - logger: any; - testApiKey: string; - liveApiKey: string; - defaultCurrency?: string; - successUrl: string; + db: any; + orgId: string; + logger: any; + testApiKey: string; + liveApiKey: string; + defaultCurrency?: string; + successUrl: string; }) => { - // 1. Check if API keys are valid - try { - await clearOrgCache({ - db, - orgId, - logger, - }); + // 1. Check if API keys are valid + try { + await clearOrgCache({ + db, + orgId, + logger, + }); - await checkKeyValid(testApiKey); - await checkKeyValid(liveApiKey); + await checkKeyValid(testApiKey); + await checkKeyValid(liveApiKey); - // Get default currency from Stripe - let stripe = new Stripe(testApiKey); + // Get default currency from Stripe + let stripe = new Stripe(testApiKey); - let account = await stripe.accounts.retrieve(); + let account = await stripe.accounts.retrieve(); - if (nullish(defaultCurrency) && nullish(account.default_currency)) { - throw new RecaseError({ - message: "Default currency not set", - code: ErrCode.StripeKeyInvalid, - statusCode: 500, - }); - } else if (nullish(defaultCurrency)) { - defaultCurrency = account.default_currency; - } - } catch (error: any) { - // console.error("Error checking stripe keys", error); - throw new RecaseError({ - message: error.message || "Invalid Stripe API keys", - code: ErrCode.StripeKeyInvalid, - statusCode: 500, - data: error, - }); - } - // 2. Create webhook endpoint - let testWebhook: Stripe.WebhookEndpoint; - let liveWebhook: Stripe.WebhookEndpoint; - try { - testWebhook = await createWebhookEndpoint( - testApiKey, - AppEnv.Sandbox, - orgId, - ); - liveWebhook = await createWebhookEndpoint(liveApiKey, AppEnv.Live, orgId); - } catch (error) { - throw new RecaseError({ - message: "Error creating stripe webhook", - code: ErrCode.StripeKeyInvalid, - statusCode: 500, - data: error, - }); - } + if (nullish(defaultCurrency) && nullish(account.default_currency)) { + throw new RecaseError({ + message: "Default currency not set", + code: ErrCode.StripeKeyInvalid, + statusCode: 500, + }); + } else if (nullish(defaultCurrency)) { + defaultCurrency = account.default_currency; + } + } catch (error: any) { + // console.error("Error checking stripe keys", error); + throw new RecaseError({ + message: error.message || "Invalid Stripe API keys", + code: ErrCode.StripeKeyInvalid, + statusCode: 500, + data: error, + }); + } + // 2. Create webhook endpoint + let testWebhook: Stripe.WebhookEndpoint; + let liveWebhook: Stripe.WebhookEndpoint; + try { + testWebhook = await createWebhookEndpoint( + testApiKey, + AppEnv.Sandbox, + orgId + ); + liveWebhook = await createWebhookEndpoint(liveApiKey, AppEnv.Live, orgId); + } catch (error) { + throw new RecaseError({ + message: "Error creating stripe webhook", + code: ErrCode.StripeKeyInvalid, + statusCode: 500, + data: error, + }); + } - return { - defaultCurrency, - stripeConfig: { - test_api_key: encryptData(testApiKey), - live_api_key: encryptData(liveApiKey), - test_webhook_secret: encryptData(testWebhook.secret as string), - live_webhook_secret: encryptData(liveWebhook.secret as string), - success_url: successUrl, - }, - }; + return { + defaultCurrency, + stripeConfig: { + test_api_key: encryptData(testApiKey), + live_api_key: encryptData(liveApiKey), + test_webhook_secret: encryptData(testWebhook.secret as string), + live_webhook_secret: encryptData(liveWebhook.secret as string), + success_url: successUrl, + }, + }; }; const connectStripeBody = z.object({ - secret_key: z.string().optional(), - success_url: z.string().optional(), - default_currency: z.string().optional(), + secret_key: z.string().optional(), + success_url: z.string().optional(), + default_currency: z.string().optional(), }); export const handleConnectStripe = async (req: any, res: any) => - routeHandler({ - req, - res, - action: "connect stripe", + routeHandler({ + req, + res, + action: "connect stripe", - handler: async (req: any, res: any) => { - // 1. Get body - const { secret_key, success_url, default_currency } = - connectStripeBody.parse(req.body); + handler: async (req: any, res: any) => { + // 1. Get body + const { secret_key, success_url, default_currency } = + connectStripeBody.parse(req.body); - if (!secret_key && !success_url && !default_currency) { - throw new RecaseError({ - message: "Missing required fields", - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } + if (!secret_key && !success_url && !default_currency) { + throw new RecaseError({ + message: "Missing required fields", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } - // 2. If secret_key present, but stripe not disconnected, throw an error - if (secret_key && isStripeConnected({ org: req.org, env: req.env })) { - throw new RecaseError({ - message: - "Please disconnect Stripe before connecting a new secret key", - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } + // 2. If secret_key present, but stripe not disconnected, throw an error + if (secret_key && isStripeConnected({ org: req.org, env: req.env })) { + throw new RecaseError({ + message: + "Please disconnect Stripe before connecting a new secret key", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } - if (success_url) { - if ( - !success_url.startsWith("http://") && - !success_url.startsWith("https://") - ) { - throw new RecaseError({ - message: `Success URL should start with http:// or https://, instead got ${success_url}`, - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } - } + if (success_url) { + if ( + !success_url.startsWith("http://") && + !success_url.startsWith("https://") + ) { + throw new RecaseError({ + message: `Success URL should start with http:// or https://, instead got ${success_url}`, + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + } - const { logger } = req; + const { logger } = req; - logger.info(`Connecting stripe for org ${req.org.slug}, ENV: ${req.env}`); + logger.info(`Connecting stripe for org ${req.org.slug}, ENV: ${req.env}`); - if (!isStripeConnected({ org: req.org, env: req.env }) && !secret_key) { - throw new RecaseError({ - message: "Please provide your stripe secret key", - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } + if (!isStripeConnected({ org: req.org, env: req.env }) && !secret_key) { + throw new RecaseError({ + message: "Please provide your stripe secret key", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } - const curOrg = structuredClone(req.org); - const isSandbox = req.env === AppEnv.Sandbox; - const curDefaultCurrency = curOrg.default_currency; + const curOrg = structuredClone(req.org); + const isSandbox = req.env === AppEnv.Sandbox; + const curDefaultCurrency = curOrg.default_currency; - // 1. Reconnect stripe - let updates: any = {}; - if (secret_key) { - const result = await connectStripe({ - orgId: req.orgId, - apiKey: secret_key!, - env: req.env, - }); + // 1. Reconnect stripe + let updates: any = {}; + if (secret_key) { + const result = await connectStripe({ + orgId: req.orgId, + apiKey: secret_key!, + env: req.env, + }); - logger.info(`Created new stripe connection`); + logger.info(`Created new stripe connection`); - updates = { - stripe_config: { - ...curOrg.stripe_config, - }, - default_currency: nullish(curDefaultCurrency) - ? result.defaultCurrency - : undefined, - }; + updates = { + stripe_config: { + ...curOrg.stripe_config, + }, + default_currency: nullish(curDefaultCurrency) + ? result.defaultCurrency + : undefined, + }; - if (isSandbox) { - updates.stripe_config.test_api_key = result.test_api_key; - updates.stripe_config.test_webhook_secret = - result.test_webhook_secret; - } else { - updates.stripe_config.live_api_key = result.live_api_key; - updates.stripe_config.live_webhook_secret = - result.live_webhook_secret; - } - } + if (isSandbox) { + updates.stripe_config.test_api_key = result.test_api_key; + updates.stripe_config.test_webhook_secret = + result.test_webhook_secret; + } else { + updates.stripe_config.live_api_key = result.live_api_key; + updates.stripe_config.live_webhook_secret = + result.live_webhook_secret; + } + } - // 2. If success url present, add it to the updates + // 2. If success url present, add it to the updates - if (success_url !== undefined && success_url !== curOrg.success_url) { - updates = { - ...updates, - stripe_config: { - ...curOrg.stripe_config, - ...(updates?.stripe_config || {}), - }, - }; + if (success_url !== undefined && success_url !== curOrg.success_url) { + updates = { + ...updates, + stripe_config: { + ...curOrg.stripe_config, + ...(updates?.stripe_config || {}), + }, + }; - if (isSandbox) { - updates.stripe_config.sandbox_success_url = success_url; - } else { - updates.stripe_config.success_url = success_url; - } + if (isSandbox) { + updates.stripe_config.sandbox_success_url = success_url; + } else { + updates.stripe_config.success_url = success_url; + } - logger.info(`Updated success URL to ${success_url}`); - } + logger.info(`Updated success URL to ${success_url}`); + } - // 3. Default currency - if (default_currency && default_currency !== curOrg.default_currency) { - updates = { - ...updates, - default_currency: default_currency, - }; + // 3. Default currency + if (default_currency && default_currency !== curOrg.default_currency) { + updates = { + ...updates, + default_currency: default_currency, + }; - logger.info(`Updated default currency to ${default_currency}`); - } + logger.info(`Updated default currency to ${default_currency}`); + } - const newOrg = await OrgService.update({ - db: req.db, - orgId: req.orgId, - updates: updates, - }); + const newOrg = await OrgService.update({ + db: req.db, + orgId: req.orgId, + updates: updates, + }); - // Initialize stripe prices... - await ensureStripeProductsWithEnv({ - db: req.db, - logger: req.logger, - req, - org: newOrg!, - env: req.env, - }); + // Initialize stripe prices... + await ensureStripeProductsWithEnv({ + db: req.db, + logger: req.logger, + req, + org: newOrg!, + env: req.env, + }); - res.status(200).json({ - message: "Stripe connected", - }); - }, - }); + res.status(200).json({ + message: "Stripe connected", + }); + }, + }); + +export const handleGetStripe = async (req: any, res: any) => { + try { + const org = await OrgService.getFromReq(req); + + const stripeCli = createStripeCli({ org, env: req.env }); + + const account_details = await stripeCli.accounts.retrieve(); + + res.status(200).json(account_details); + } catch (error) { + handleRequestError({ req, error, res, action: "Get invoice" }); + } +}; diff --git a/server/src/internal/orgs/orgRouter.ts b/server/src/internal/orgs/orgRouter.ts index d662f7573..052bc2e9e 100644 --- a/server/src/internal/orgs/orgRouter.ts +++ b/server/src/internal/orgs/orgRouter.ts @@ -10,7 +10,10 @@ 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 } from "./handlers/handleConnectStripe.js"; +import { + handleConnectStripe, + handleGetStripe, +} from "./handlers/handleConnectStripe.js"; import { handleDeleteStripe } from "./handlers/handleDeleteStripe.js"; import { handleGetOrg } from "./handlers/handleGetOrg.js"; @@ -29,6 +32,8 @@ orgRouter.delete("/delete-user", async (req: any, res) => { orgRouter.get("", handleGetOrg); +orgRouter.get("/stripe", handleGetStripe); + orgRouter.post("/stripe", handleConnectStripe); orgRouter.delete("/stripe", handleDeleteStripe); From aecb45113a89bca4a274d1896ef4859f14b445c5 Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Tue, 23 Sep 2025 04:57:33 +0530 Subject: [PATCH 2/3] changes for the frontend logic to introduce the new deep-link-url format --- vite/src/utils/linkUtils.ts | 42 +-- .../customer-sidebar/CustomerDetails.tsx | 251 ++++++++++-------- 2 files changed, 172 insertions(+), 121 deletions(-) diff --git a/vite/src/utils/linkUtils.ts b/vite/src/utils/linkUtils.ts index a3b9b4694..577e87310 100644 --- a/vite/src/utils/linkUtils.ts +++ b/vite/src/utils/linkUtils.ts @@ -1,24 +1,36 @@ import { AppEnv } from "@autumn/shared"; -export const getStripeCusLink = (customerId: string, env: AppEnv) => { - return `https://dashboard.stripe.com${ - env == AppEnv.Live ? "" : "/test" - }/customers/${customerId}`; +export const getStripeCusLink = ( + customerId: string, + env: AppEnv, + accountId?: string +) => { + return `https://dashboard.stripe.com${ + env == AppEnv.Live ? "" : "/test" + }/${accountId}/customers/${customerId}`; }; -export const getStripeSubLink = (subscriptionId: string, env: AppEnv) => { - return `https://dashboard.stripe.com${ - env == AppEnv.Live ? "" : "/test" - }/subscriptions/${subscriptionId}`; +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) => { - return `https://dashboard.stripe.com${ - env == AppEnv.Live ? "" : "/test" - }/subscription_schedules/${scheduledId}`; +export const getStripeSubScheduleLink = ( + scheduledId: string, + env: AppEnv, + accountId?: string +) => { + return `https://dashboard.stripe.com${ + env == AppEnv.Live ? "" : "/test" + }/${accountId}/subscription_schedules/${scheduledId}`; }; export const getStripeInvoiceLink = (stripeInvoice: any) => { - return `https://dashboard.stripe.com${ - stripeInvoice.livemode ? "" : "/test" - }/invoices/${stripeInvoice.id || stripeInvoice.stripe_id}`; + return `https://dashboard.stripe.com${ + stripeInvoice.livemode ? "" : "/test" + }/invoices/${stripeInvoice.id || stripeInvoice.stripe_id}`; }; 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 f9cf78b4e..df1cca754 100644 --- a/vite/src/views/customers/customer/components/customer-sidebar/CustomerDetails.tsx +++ b/vite/src/views/customers/customer/components/customer-sidebar/CustomerDetails.tsx @@ -10,119 +10,158 @@ 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"; 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 { customer } = useCusQuery(); + const env = useEnv(); + const { org } = useOrg(); - return ( -
- -
- ID -
-
- {customer.id ? ( - - {customer.id} - - ) : ( - - )} -
-
+ const axiosInstance = useAxiosInstance(); - Name -
- -
+ const getStripeAccountInfo = async () => { + try { + const { data } = await axiosInstance.get(`/organization/stripe`); + return data; + } catch (error) { + toast.error("Failed to get invoice URL"); + return null; + } + }; - Email -
- -
+ return ( +
+ +
+ ID +
+
+ {customer.id ? ( + + {customer.id} + + ) : ( + + )} +
+
- - Fingerprint - -
- -
+ Name +
+ +
- {customer.processor?.id && ( - <> - - Stripe - -
- -
- -
- -
- - )} -
-
-
- ); + Email +
+ +
+ + + Fingerprint + +
+ +
+ + {customer.processor?.id && ( + <> + + Stripe + +
+
+
+ +
+
+
+ + )} +
+
+
+ ); }; From a733e2f899326b113b80acc269f88a9419450be3 Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Tue, 23 Sep 2025 05:07:42 +0530 Subject: [PATCH 3/3] changes for the product section to implement the new deep stripe link logic --- .../CusProductStripeLink.tsx | 143 +++++++++++------- 1 file changed, 87 insertions(+), 56 deletions(-) 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 bfb130bb6..ec2a18ddd 100644 --- a/vite/src/views/customers/customer/customer-product-list/CusProductStripeLink.tsx +++ b/vite/src/views/customers/customer/customer-product-list/CusProductStripeLink.tsx @@ -1,67 +1,98 @@ +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 { ArrowUpRightFromSquare } from "lucide-react"; import React from "react"; import { Link } from "react-router"; +import { toast } from "sonner"; export const CusProductStripeLink = ({ - cusProduct, + cusProduct, }: { - cusProduct: FullCusProduct; + cusProduct: FullCusProduct; }) => { - const env = useEnv(); - return ( - <> - {cusProduct.subscription_ids && - cusProduct.subscription_ids.length > 0 && ( - - {cusProduct.subscription_ids.map((subId: string) => { - return ( - { - e.stopPropagation(); - }} - > -
- -
- - ); - })} -
- )} - {cusProduct.status == CusProductStatus.Scheduled && - cusProduct.scheduled_ids && - cusProduct.scheduled_ids.length > 0 && ( - - {cusProduct.scheduled_ids.map((subId: string) => { - return ( - { - e.stopPropagation(); - }} - > -
- -
- - ); - })} -
- )} - - ); + 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)); + } + }} + > +
+ +
+
+ ); + })} +
+ )} + + ); };