feat: add setup payment v2
This commit is contained in:
6
server/src/external/autumn/autumnCli.ts
vendored
6
server/src/external/autumn/autumnCli.ts
vendored
@@ -30,7 +30,7 @@ import {
|
|||||||
type ProductItem,
|
type ProductItem,
|
||||||
type RewardRedemption,
|
type RewardRedemption,
|
||||||
type SetUsageParams,
|
type SetUsageParams,
|
||||||
type SetupPaymentParamsV0,
|
type SetupPaymentParamsV1,
|
||||||
type TrackParams,
|
type TrackParams,
|
||||||
type UpdateBalanceParamsV0,
|
type UpdateBalanceParamsV0,
|
||||||
type UpdateSubscriptionV0Params,
|
type UpdateSubscriptionV0Params,
|
||||||
@@ -908,8 +908,8 @@ export class AutumnInt {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
setupPayment: async (params: SetupPaymentParamsV0) => {
|
setupPayment: async (params: SetupPaymentParamsV1) => {
|
||||||
const data = await this.post(`/setup_payment`, params);
|
const data = await this.post(`/billing.setup_payment`, params);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
30
server/src/external/stripe/stripeCusUtils.ts
vendored
30
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -280,3 +280,33 @@ const deleteAllStripeCustomers = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the customer's payment method and sets it as their default for invoices.
|
||||||
|
* Returns the payment method if found and set, or null if none available.
|
||||||
|
*/
|
||||||
|
export const updateDefaultPaymentMethod = async ({
|
||||||
|
stripeCli,
|
||||||
|
stripeCustomerId,
|
||||||
|
}: {
|
||||||
|
stripeCli: Stripe;
|
||||||
|
stripeCustomerId: string;
|
||||||
|
}) => {
|
||||||
|
const paymentMethod = await getCusPaymentMethod({
|
||||||
|
stripeCli,
|
||||||
|
stripeId: stripeCustomerId,
|
||||||
|
errorIfNone: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!paymentMethod) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await stripeCli.customers.update(stripeCustomerId, {
|
||||||
|
invoice_settings: {
|
||||||
|
default_payment_method: paymentMethod.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return paymentMethod;
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { handleCheckoutSessionMetadataV2 } from "@/external/stripe/webhookHandle
|
|||||||
import type { StripeWebhookContext } from "../../webhookMiddlewares/stripeWebhookContext.js";
|
import type { StripeWebhookContext } from "../../webhookMiddlewares/stripeWebhookContext.js";
|
||||||
import { setupCheckoutSessionCompletedContext } from "./setupCheckoutSessionCompletedContext.js";
|
import { setupCheckoutSessionCompletedContext } from "./setupCheckoutSessionCompletedContext.js";
|
||||||
import { handleLegacyCheckoutSessionMetadata } from "./tasks/handleLegacyCheckoutSessionMetadata.ts/handleCheckoutSessionCompletedLegacy.js";
|
import { handleLegacyCheckoutSessionMetadata } from "./tasks/handleLegacyCheckoutSessionMetadata.ts/handleCheckoutSessionCompletedLegacy.js";
|
||||||
|
import { handleSetupPaymentMetadata } from "./tasks/handleSetupPaymentMetadata.js";
|
||||||
import { handleStandaloneSetupCheckout } from "./tasks/handleStandaloneSetupCheckout.js";
|
import { handleStandaloneSetupCheckout } from "./tasks/handleStandaloneSetupCheckout.js";
|
||||||
import { updateCustomerFromCheckout } from "./tasks/updateCustomerFromCheckout.js";
|
import { updateCustomerFromCheckout } from "./tasks/updateCustomerFromCheckout.js";
|
||||||
|
|
||||||
@@ -24,6 +25,12 @@ export const handleStripeCheckoutSessionCompleted = async ({
|
|||||||
checkoutContext,
|
checkoutContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Setup payment with metadata (plan attachment after setup)
|
||||||
|
await handleSetupPaymentMetadata({
|
||||||
|
ctx,
|
||||||
|
checkoutContext,
|
||||||
|
});
|
||||||
|
|
||||||
// Legacy flow
|
// Legacy flow
|
||||||
await handleLegacyCheckoutSessionMetadata({
|
await handleLegacyCheckoutSessionMetadata({
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { type DeferredSetupPaymentData, MetadataType } from "@autumn/shared";
|
||||||
|
import { createStripeCli } from "@/external/connect/createStripeCli";
|
||||||
|
import { updateDefaultPaymentMethod } from "@/external/stripe/stripeCusUtils";
|
||||||
|
import { billingActions } from "@/internal/billing/v2/actions";
|
||||||
|
import { setupPaymentToAttachParams } from "@/internal/billing/v2/actions/setupPayment/setupPaymentUtils";
|
||||||
|
import { MetadataService } from "@/internal/metadata/MetadataService";
|
||||||
|
import type { StripeWebhookContext } from "../../../webhookMiddlewares/stripeWebhookContext";
|
||||||
|
import type { CheckoutSessionCompletedContext } from "../setupCheckoutSessionCompletedContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles setup checkout sessions with metadata (plan attachment after setup).
|
||||||
|
* Updates the customer's payment method, then attaches the plan if specified.
|
||||||
|
*/
|
||||||
|
export const handleSetupPaymentMetadata = async ({
|
||||||
|
ctx,
|
||||||
|
checkoutContext,
|
||||||
|
}: {
|
||||||
|
ctx: StripeWebhookContext;
|
||||||
|
checkoutContext: CheckoutSessionCompletedContext;
|
||||||
|
}): Promise<void> => {
|
||||||
|
const { org, env, logger } = ctx;
|
||||||
|
const { stripeCheckoutSession, metadata } = checkoutContext;
|
||||||
|
|
||||||
|
if (metadata?.type !== MetadataType.SetupPaymentV2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`[checkout.completed] Handling setup payment metadata: ${metadata.id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const deferredData = metadata.data as DeferredSetupPaymentData;
|
||||||
|
const stripeCustomerId = stripeCheckoutSession.customer as string;
|
||||||
|
|
||||||
|
if (!stripeCustomerId) {
|
||||||
|
logger.warn("Setup payment metadata: no Stripe customer ID, skipping");
|
||||||
|
await MetadataService.delete({ db: ctx.db, id: metadata.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Update customer's default payment method
|
||||||
|
const stripeCli = createStripeCli({ org, env });
|
||||||
|
const paymentMethod = await updateDefaultPaymentMethod({
|
||||||
|
stripeCli,
|
||||||
|
stripeCustomerId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (paymentMethod) {
|
||||||
|
logger.info(
|
||||||
|
`Setup payment metadata: set default payment method for ${stripeCustomerId}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn("Setup payment metadata: no payment method found after setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Attach plan if plan_id was specified
|
||||||
|
if (deferredData.params.plan_id) {
|
||||||
|
logger.info(
|
||||||
|
`Setup payment metadata: attaching plan ${deferredData.params.plan_id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const attachParams = setupPaymentToAttachParams({
|
||||||
|
params: deferredData.params,
|
||||||
|
});
|
||||||
|
|
||||||
|
await billingActions.attach({
|
||||||
|
ctx,
|
||||||
|
params: attachParams,
|
||||||
|
preview: false,
|
||||||
|
skipAutumnCheckout: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Setup payment metadata: plan ${deferredData.params.plan_id} attached successfully`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Cleanup metadata
|
||||||
|
await MetadataService.delete({ db: ctx.db, id: metadata.id });
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||||
import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
import { updateDefaultPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||||
import { CusService } from "@/internal/customers/CusService.js";
|
import { CusService } from "@/internal/customers/CusService.js";
|
||||||
import type { StripeWebhookContext } from "../../../webhookMiddlewares/stripeWebhookContext.js";
|
import type { StripeWebhookContext } from "../../../webhookMiddlewares/stripeWebhookContext.js";
|
||||||
import type { CheckoutSessionCompletedContext } from "../setupCheckoutSessionCompletedContext.js";
|
import type { CheckoutSessionCompletedContext } from "../setupCheckoutSessionCompletedContext.js";
|
||||||
@@ -42,11 +42,9 @@ export const handleStandaloneSetupCheckout = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stripeCli = createStripeCli({ org, env });
|
const stripeCli = createStripeCli({ org, env });
|
||||||
|
const paymentMethod = await updateDefaultPaymentMethod({
|
||||||
const paymentMethod = await getCusPaymentMethod({
|
|
||||||
stripeCli,
|
stripeCli,
|
||||||
stripeId: stripeCustomerId,
|
stripeCustomerId,
|
||||||
errorIfNone: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!paymentMethod) {
|
if (!paymentMethod) {
|
||||||
@@ -57,13 +55,6 @@ export const handleStandaloneSetupCheckout = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Standalone setup checkout: updating default payment method for customer ${customer.id}`,
|
`Standalone setup checkout: updated default payment method for customer ${customer.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set as customer's default payment method
|
|
||||||
await stripeCli.customers.update(stripeCustomerId, {
|
|
||||||
invoice_settings: {
|
|
||||||
default_payment_method: paymentMethod.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { handleCheckoutV2 } from "./checkout/handleCheckoutV2.js";
|
|||||||
import { handleSetupPayment } from "./handlers/handleSetupPayment.js";
|
import { handleSetupPayment } from "./handlers/handleSetupPayment.js";
|
||||||
import { handleAttachV2 } from "./v2/handlers/handleAttachV2.js";
|
import { handleAttachV2 } from "./v2/handlers/handleAttachV2.js";
|
||||||
import { handlePreviewUpdateSubscription } from "./v2/handlers/handlePreviewUpdateSubscription.js";
|
import { handlePreviewUpdateSubscription } from "./v2/handlers/handlePreviewUpdateSubscription.js";
|
||||||
|
import { handleSetupPaymentV2 } from "./v2/handlers/handleSetupPaymentV2.js";
|
||||||
import { handleUpdateSubscription } from "./v2/handlers/handleUpdateSubscription.js";
|
import { handleUpdateSubscription } from "./v2/handlers/handleUpdateSubscription.js";
|
||||||
|
|
||||||
export const billingRouter = new Hono<HonoEnv>();
|
export const billingRouter = new Hono<HonoEnv>();
|
||||||
@@ -29,6 +30,7 @@ billingRpcRouter.post(
|
|||||||
);
|
);
|
||||||
billingRpcRouter.post("/billing.attach", ...handleAttachV2);
|
billingRpcRouter.post("/billing.attach", ...handleAttachV2);
|
||||||
billingRpcRouter.post("/billing.preview_attach", ...handlePreviewAttach);
|
billingRpcRouter.post("/billing.preview_attach", ...handlePreviewAttach);
|
||||||
|
billingRpcRouter.post("/billing.setup_payment", ...handleSetupPaymentV2);
|
||||||
billingRpcRouter.post(
|
billingRpcRouter.post(
|
||||||
"/billing.open_customer_portal",
|
"/billing.open_customer_portal",
|
||||||
...handleOpenCustomerPortalV2,
|
...handleOpenCustomerPortalV2,
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import { legacyAttach } from "@/internal/billing/v2/actions/legacy/legacyAttach"
|
|||||||
import { renew } from "@/internal/billing/v2/actions/legacy/renew";
|
import { renew } from "@/internal/billing/v2/actions/legacy/renew";
|
||||||
import { updateQuantity } from "@/internal/billing/v2/actions/legacy/updateQuantity";
|
import { updateQuantity } from "@/internal/billing/v2/actions/legacy/updateQuantity";
|
||||||
import { migrate } from "@/internal/billing/v2/actions/migrate/migrate";
|
import { migrate } from "@/internal/billing/v2/actions/migrate/migrate";
|
||||||
|
import { setupPayment } from "@/internal/billing/v2/actions/setupPayment/setupPayment";
|
||||||
import { updateSubscription } from "@/internal/billing/v2/actions/updateSubscription/updateSubscription";
|
import { updateSubscription } from "@/internal/billing/v2/actions/updateSubscription/updateSubscription";
|
||||||
|
|
||||||
export const billingActions = {
|
export const billingActions = {
|
||||||
attach: attach,
|
attach: attach,
|
||||||
|
setupPayment: setupPayment,
|
||||||
updateSubscription: updateSubscription,
|
updateSubscription: updateSubscription,
|
||||||
migrate: migrate,
|
migrate: migrate,
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
type Customer,
|
||||||
|
type DeferredSetupPaymentData,
|
||||||
|
MetadataType,
|
||||||
|
type SetupPaymentParamsV1,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { addDays } from "date-fns";
|
||||||
|
import type Stripe from "stripe";
|
||||||
|
import { createStripeCli } from "@/external/connect/createStripeCli";
|
||||||
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
|
import { createStripeSessionWithCardFallback } from "@/internal/billing/v2/providers/stripe/utils/checkoutSessions/createStripeSessionWithCardFallback";
|
||||||
|
import { MetadataService } from "@/internal/metadata/MetadataService";
|
||||||
|
import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils";
|
||||||
|
import { generateId } from "@/utils/genUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts deferred metadata so the webhook can attach the plan after setup completes.
|
||||||
|
*/
|
||||||
|
const insertSetupPaymentMetadata = async ({
|
||||||
|
ctx,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
ctx: AutumnContext;
|
||||||
|
params: SetupPaymentParamsV1;
|
||||||
|
}) => {
|
||||||
|
const payload: DeferredSetupPaymentData = {
|
||||||
|
requestId: ctx.id,
|
||||||
|
orgId: ctx.org.id,
|
||||||
|
env: ctx.env,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
|
||||||
|
return MetadataService.insert({
|
||||||
|
db: ctx.db,
|
||||||
|
data: {
|
||||||
|
id: generateId("meta"),
|
||||||
|
type: MetadataType.SetupPaymentV2,
|
||||||
|
data: payload,
|
||||||
|
created_at: Date.now(),
|
||||||
|
expires_at: addDays(Date.now(), 10).getTime(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Stripe checkout session in setup mode.
|
||||||
|
* If plan_id is specified, stores metadata so the webhook can attach the plan after setup.
|
||||||
|
*/
|
||||||
|
export const createSetupCheckoutSession = async ({
|
||||||
|
ctx,
|
||||||
|
customer,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
ctx: AutumnContext;
|
||||||
|
customer: Customer;
|
||||||
|
params: SetupPaymentParamsV1;
|
||||||
|
}) => {
|
||||||
|
const { org, env, logger } = ctx;
|
||||||
|
const stripeCli = createStripeCli({ org, env });
|
||||||
|
|
||||||
|
// 1. Insert metadata (if plan_id specified)
|
||||||
|
const metadata = params.plan_id
|
||||||
|
? await insertSetupPaymentMetadata({ ctx, params })
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 2. Build session params
|
||||||
|
const fullParams: Stripe.Checkout.SessionCreateParams = {
|
||||||
|
customer: customer.processor?.id ?? undefined,
|
||||||
|
mode: "setup",
|
||||||
|
success_url: params.success_url || toSuccessUrl({ org, env }),
|
||||||
|
currency: org.default_currency || "usd",
|
||||||
|
...params.checkout_session_params,
|
||||||
|
...(metadata ? { metadata: { autumn_metadata_id: metadata.id } } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Create session with card-type fallback
|
||||||
|
const session = await createStripeSessionWithCardFallback({
|
||||||
|
stripeCli,
|
||||||
|
params: fullParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Created setup checkout session for ${customer.id ?? customer.internal_id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Link metadata to checkout session
|
||||||
|
if (metadata) {
|
||||||
|
await MetadataService.update({
|
||||||
|
db: ctx.db,
|
||||||
|
id: metadata.id,
|
||||||
|
updates: { stripe_checkout_session_id: session.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url: session.url };
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type { SetupPaymentParamsV1 } from "@autumn/shared";
|
||||||
|
import { getOrCreateStripeCustomer } from "@/external/stripe/customers/operations/getOrCreateStripeCustomer";
|
||||||
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
|
import { billingActions } from "@/internal/billing/v2/actions";
|
||||||
|
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer";
|
||||||
|
import { createSetupCheckoutSession } from "./createSetupCheckoutSession";
|
||||||
|
import { setupPaymentToAttachParams } from "./setupPaymentUtils";
|
||||||
|
|
||||||
|
export interface SetupPaymentResult {
|
||||||
|
customer_id: string;
|
||||||
|
entity_id?: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Stripe checkout session in setup mode.
|
||||||
|
* If plan_id is specified, validates the plan via preview and attaches it after setup completes.
|
||||||
|
*/
|
||||||
|
export const setupPayment = async ({
|
||||||
|
ctx,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
ctx: AutumnContext;
|
||||||
|
params: SetupPaymentParamsV1;
|
||||||
|
}): Promise<SetupPaymentResult> => {
|
||||||
|
const { logger } = ctx;
|
||||||
|
|
||||||
|
// 1. Get or create customer (+ Stripe customer)
|
||||||
|
const fullCustomer = await getOrCreateCustomer({
|
||||||
|
ctx,
|
||||||
|
customerId: params.customer_id,
|
||||||
|
customerData: params.customer_data,
|
||||||
|
entityId: params.entity_id,
|
||||||
|
entityData: params.entity_data,
|
||||||
|
});
|
||||||
|
|
||||||
|
await getOrCreateStripeCustomer({
|
||||||
|
ctx,
|
||||||
|
customer: fullCustomer,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. If plan_id specified, run attach in preview mode to validate
|
||||||
|
if (params.plan_id) {
|
||||||
|
logger.info(`Setup payment: validating plan ${params.plan_id} via preview`);
|
||||||
|
|
||||||
|
const attachParams = setupPaymentToAttachParams({ params });
|
||||||
|
|
||||||
|
await billingActions.attach({
|
||||||
|
ctx,
|
||||||
|
params: attachParams,
|
||||||
|
preview: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Setup payment: plan ${params.plan_id} validated successfully`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Create Stripe setup checkout session
|
||||||
|
const { url } = await createSetupCheckoutSession({
|
||||||
|
ctx,
|
||||||
|
customer: fullCustomer,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
customer_id: fullCustomer.id ?? fullCustomer.internal_id,
|
||||||
|
entity_id: params.entity_id,
|
||||||
|
url: url ?? "",
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { AttachParamsV1, SetupPaymentParamsV1 } from "@autumn/shared";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts setup payment params to attach params for the preview/attach call.
|
||||||
|
*/
|
||||||
|
export const setupPaymentToAttachParams = ({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: SetupPaymentParamsV1;
|
||||||
|
}): AttachParamsV1 => ({
|
||||||
|
...params,
|
||||||
|
plan_id: params.plan_id as string,
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
AffectedResource,
|
||||||
|
ApiVersion,
|
||||||
|
SetupPaymentParamsV0Schema,
|
||||||
|
SetupPaymentParamsV1Schema,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { createRoute } from "@/honoMiddlewares/routeHandler";
|
||||||
|
import { billingActions } from "@/internal/billing/v2/actions";
|
||||||
|
|
||||||
|
export const handleSetupPaymentV2 = createRoute({
|
||||||
|
versionedBody: {
|
||||||
|
latest: SetupPaymentParamsV1Schema,
|
||||||
|
[ApiVersion.V1_Beta]: SetupPaymentParamsV0Schema,
|
||||||
|
},
|
||||||
|
resource: AffectedResource.Customer,
|
||||||
|
handler: async (c) => {
|
||||||
|
const ctx = c.get("ctx");
|
||||||
|
const body = c.req.valid("json");
|
||||||
|
|
||||||
|
const result = await billingActions.setupPayment({
|
||||||
|
ctx,
|
||||||
|
params: body,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(result, 200);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import { addDays } from "date-fns";
|
|||||||
import type Stripe from "stripe";
|
import type Stripe from "stripe";
|
||||||
import { createStripeCli } from "@/external/connect/createStripeCli";
|
import { createStripeCli } from "@/external/connect/createStripeCli";
|
||||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||||
|
import { createStripeSessionWithCardFallback } from "@/internal/billing/v2/providers/stripe/utils/checkoutSessions/createStripeSessionWithCardFallback";
|
||||||
import {
|
import {
|
||||||
insertMetadataFromBillingPlan,
|
insertMetadataFromBillingPlan,
|
||||||
updateMetadataWithCheckoutSession,
|
updateMetadataWithCheckoutSession,
|
||||||
@@ -60,28 +61,15 @@ export const executeStripeCheckoutSessionAction = async ({
|
|||||||
metadata: { autumn_metadata_id: metadata.id },
|
metadata: { autumn_metadata_id: metadata.id },
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Create checkout session with fallback for payment method types
|
// 3. Create checkout session with card-type fallback
|
||||||
let stripeCheckoutSession: Stripe.Checkout.Session;
|
const stripeCheckoutSession = await createStripeSessionWithCardFallback({
|
||||||
try {
|
stripeCli,
|
||||||
stripeCheckoutSession =
|
params: fullParams,
|
||||||
await stripeCli.checkout.sessions.create(fullParams);
|
});
|
||||||
logger.info(
|
|
||||||
`✅ Created checkout session for customer ${fullCustomer.id ?? fullCustomer.internal_id}`,
|
logger.info(
|
||||||
);
|
`Created checkout session for customer ${fullCustomer.id ?? fullCustomer.internal_id}`,
|
||||||
} catch (error) {
|
);
|
||||||
const msg = error instanceof Error ? error.message : undefined;
|
|
||||||
if (msg?.includes("No valid payment method types")) {
|
|
||||||
stripeCheckoutSession = await stripeCli.checkout.sessions.create({
|
|
||||||
...fullParams,
|
|
||||||
payment_method_types: ["card"],
|
|
||||||
});
|
|
||||||
logger.info(
|
|
||||||
"✅ Created fallback checkout session with card payment method",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Update metadata with checkout session ID
|
// 4. Update metadata with checkout session ID
|
||||||
await updateMetadataWithCheckoutSession({
|
await updateMetadataWithCheckoutSession({
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type Stripe from "stripe";
|
||||||
|
import type { createStripeCli } from "@/external/connect/createStripeCli";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Stripe checkout session, retrying with explicit `payment_method_types: ["card"]`
|
||||||
|
* if Stripe rejects automatic payment method determination.
|
||||||
|
*/
|
||||||
|
export const createStripeSessionWithCardFallback = async ({
|
||||||
|
stripeCli,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
stripeCli: ReturnType<typeof createStripeCli>;
|
||||||
|
params: Stripe.Checkout.SessionCreateParams;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
return await stripeCli.checkout.sessions.create(params);
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : undefined;
|
||||||
|
|
||||||
|
if (msg?.includes("payment method") || msg?.includes("No valid payment")) {
|
||||||
|
return stripeCli.checkout.sessions.create({
|
||||||
|
...params,
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { SetupPaymentParamsV1 } from "@api/billing/setupPayment/setupPaymentParamsV1";
|
||||||
import {
|
import {
|
||||||
type AppEnv,
|
type AppEnv,
|
||||||
CusProductStatus,
|
CusProductStatus,
|
||||||
@@ -93,3 +94,10 @@ export type DeferredAutumnBillingPlanData = {
|
|||||||
billingContext: BillingContext;
|
billingContext: BillingContext;
|
||||||
resumeAfter?: StripeBillingStage;
|
resumeAfter?: StripeBillingStage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeferredSetupPaymentData = {
|
||||||
|
requestId: string;
|
||||||
|
orgId: string;
|
||||||
|
env: AppEnv;
|
||||||
|
params: SetupPaymentParamsV1;
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import {
|
|||||||
type AutumnBillingPlan,
|
type AutumnBillingPlan,
|
||||||
AutumnBillingPlanSchema,
|
AutumnBillingPlanSchema,
|
||||||
type DeferredAutumnBillingPlanData,
|
type DeferredAutumnBillingPlanData,
|
||||||
|
type DeferredSetupPaymentData,
|
||||||
} from "./autumnBillingPlan";
|
} from "./autumnBillingPlan";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AutumnBillingPlan,
|
AutumnBillingPlan,
|
||||||
DeferredAutumnBillingPlanData,
|
DeferredAutumnBillingPlanData,
|
||||||
|
DeferredSetupPaymentData,
|
||||||
StripeBillingPlan,
|
StripeBillingPlan,
|
||||||
StripeCheckoutSessionAction,
|
StripeCheckoutSessionAction,
|
||||||
StripeInvoiceAction,
|
StripeInvoiceAction,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export enum MetadataType {
|
|||||||
|
|
||||||
DeferredInvoice = "deferred_invoice",
|
DeferredInvoice = "deferred_invoice",
|
||||||
CheckoutSessionV2 = "checkout_session_v2",
|
CheckoutSessionV2 = "checkout_session_v2",
|
||||||
|
SetupPaymentV2 = "setup_payment_v2",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = pgTable("metadata", {
|
export const metadata = pgTable("metadata", {
|
||||||
|
|||||||
Reference in New Issue
Block a user