具有试用期的 Stripe 订阅即使付款失败也会进入试用状态

问题描述 投票:0回答:1

我最近从条纹卡元素迁移到新的 PaymentElements,这样我就可以支持 Apple Pay 和 Google Pay 以及我的订阅卡。我一直在浏览 stripes 文档来了解如何正确设置订阅。

现在,我正在用户旅程的一开始就设置一个客户,然后让他们填写信息和付款信息,然后在我的表单提交中,我提交用户,后端创建用户,然后创建订阅,webhooks 完成其余的工作。我遇到的问题是,当卡付款失败时,付款状态会直接进入我的网络钩子中的尾部。尽管我猜设置意图在技术上是失败的。我怎么能让它不完整,就像没有免费试用期一样。

我也必须改变我的整个流程,如果付款失败时没有在我的数据库中创建用户(像以前一样),我会更喜欢它,因为我可以在我的后端确认付款,这很重要更好的是,现在我无论如何都必须创建一个用户,并在订阅不完整时检索订阅。真的感觉我现在把我的东西搞砸了,如果有一种方法在我确认它有效后创建一个用户那就更好了。无需使用用户参数填充整个redirect_url。

后端代码。

  createSubscriptionAndUser: protectedProcedure
.input(inputSchemaCreateSubscription)
.mutation(async ({ ctx, input }) => {
  const planPriceIds = {
    Silver: env.STRIPE_PRICE_ID_SILVER,
    Gold: env.STRIPE_PRICE_ID_GOLD,
    Platinum: env.STRIPE_PRICE_ID_PLATINUM,
  };

  const clerkUser = ctx.auth.userId
    ? await clerkClient.users.getUser(ctx.auth.userId)
    : null;

  const gymsToConnect = input.gyms.map((gym) => ({ id: gym.value }));
  const trainingToConnect = input.training.map((training) => ({
    id: training.value,
  }));

  const selectedPlanFromUser: keyof typeof planPriceIds = input.plan;

  let customer;
  const email = clerkUser?.emailAddresses[0]?.emailAddress ?? "";

  const customers = await ctx.stripe.customers.list({ email });

  let customerId;
  if (customers?.data?.[0]?.id) {
    customerId = customers.data[0].id;
  }

  if (customerId) {
    customer = await ctx.stripe.customers.update(customerId, {
      name: `${clerkUser?.firstName ?? input.firstName} ${
        clerkUser?.lastName ?? input.lastName
      }`,
      metadata: {
        userId: ctx.auth.userId,
      },
    });
  } else {
    console.error("something bad happened");
    throw new TRPCError({ code: "BAD_REQUEST" });
  }

  const priceId = planPriceIds[selectedPlanFromUser];

  if (!clerkUser?.firstName || !clerkUser?.lastName) {
    await clerkClient.users.updateUser(ctx.auth.userId, {
      firstName: input.firstName,
      lastName: input.lastName,
    });
  }

  const doesUserAlreadyExist =
    await ctx.prisma.personalTrainerUser.findFirst({
      where: {
        id: ctx.auth.userId,
      },
    });

  if (
    doesUserAlreadyExist?.stripeSubscriptionId &&
    doesUserAlreadyExist?.subscriptionStatus === "incomplete"
  ) {
    const subscription = await ctx.stripe.subscriptions.retrieve(
      doesUserAlreadyExist.stripeSubscriptionId,
      {
        expand: ["latest_invoice.payment_intent", "pending_setup_intent"],
      }
    );
    const subscriptionPayload = getSubscriptionPayload(subscription);

    return { user: doesUserAlreadyExist, subscriptionPayload };
  }

  const user = await ctx.prisma.personalTrainerUser.create({
    data: {
      id: ctx.auth.userId,
      stripeCustomerId: customer.id,
      avatar: clerkUser?.imageUrl,
      email,
      firstName: clerkUser?.firstName ?? input.firstName,
      lastName: clerkUser?.lastName ?? input.lastName,
      gender: input.gender,
      gyms: {
        connect: gymsToConnect,
      },
      expertise: {
        connect: trainingToConnect,
      },
      longitude: input.longitude,
      latitude: input.latitude,
      locationName: input.locationName,
      membershipLevel: input.plan,
      holiday: false,
    },
  });

  const subscription = await ctx.stripe.subscriptions.create({
    customer: customer.id,
    items: [
      {
        price: priceId,
      },
    ],
    expand: ["latest_invoice.payment_intent", "pending_setup_intent"],
    payment_behavior: "default_incomplete",
    payment_settings: { save_default_payment_method: "on_subscription" },
    trial_period_days: 30,
    metadata: {
      userId: ctx.auth.userId,
    },
  });
  const subscriptionPayload = getSubscriptionPayload(subscription);

  return { user, subscriptionPayload };
}),

前端提交

const onSubmit = async (data: FormInputs) => {
if (currentStep === 1) {
  setCurrentStep(2);
} else {
  if (!stripe || !elements) {
    return;
  }
  setPaymentError(null); // Reset payment error state before processing

  const { error: submitError } = await elements.submit();
  if (submitError?.message) {
    setPaymentError(submitError.message);

    return;
  }

  const redirectUrl = window.location.origin + `/payment`;

  try {
    const { subscriptionPayload } = await createUserAndSubscriptionMutation(
      {
        plan: data.plan,
        gender: data.gender.value,
        longitude: data.location.longitude,
        latitude: data.location.latitude,
        locationName: data.location.locationName,
        gyms: data.gyms,
        training: data.training,
        firstName: user?.firstName ?? data.firstName,
        lastName: user?.lastName ?? data.lastName,
      }
    );
    setPaymentLoading(true);

    const confirmIntent =
      subscriptionPayload.type === "setup"
        ? stripe.confirmSetup.bind(stripe)
        : stripe.confirmPayment.bind(stripe);

    const { error } = await confirmIntent({
      elements,
      clientSecret: subscriptionPayload.clientSecret as string,
      confirmParams: {
        return_url: redirectUrl,
      },
    });
    if (error) {
      setPaymentLoading(false);
      console.error("Stripe error:", error.message);
      setPaymentError(
        error.message || "An error occurred while confirming payment."
      );
      setPaymentLoading(false);
    }
  } catch (error: unknown) {
    if (error instanceof TRPCClientError) {
      setPaymentError(error.message);
      console.error("tRPC error:", error.message);
      console.error("Error shape:", error.shape);
    } else if (error instanceof Error) {
      console.error("Some other error:", error.message);
    } else {
      console.error("Unknown error:", error);
    }
  }
}

};

next.js stripe-payments trpc
1个回答
0
投票

对于自定义元素流(您首先创建订阅,然后收集付款方式以及第一个意图),这是一个已知的限制。
据我所知,控制这一点的唯一功能是

trial_settings.missing_payment_method=cancel
,但这仍然不是你想要的:

  • 如果客户未成功完成第一个设置意图,您希望订阅取消或处于非活动状态(即不试用)。
  • trial_settings.missing_payment_method=cancel
    不会这样做,但如果第一张付款发票没有收取任何方法,它会自动取消订阅。

更让您沮丧的是,Stripe 允许这样做,但使用 Checkout - 使用

payment_method_collection=always
,除非客户完成会话 - 这需要收集有效的付款方式,否则不会创建订阅。
https://docs.stripe.com/billing/subscriptions/build-subscriptions?platform=web&ui=stripe-hosted

我建议解决这个问题的方法是放弃新的流程(首先创建订阅,然后收集付款方式以及第一个意图),而使用更老式的流程,在其中收集首先使用付款方式,然后创建您的订阅。正如您所说,这将是一个很好的“在确认有效后创建用户的方法”。

如果成功:

  • 通过试用和这些参数创建订阅:
    default_payment_method=setup_intent.payment_method

    off_session=true

    off_session
    论点与试验并不真正相关 - 避免首次付款要求身份验证很有用,但如果您没有首次付款,这自然不是问题。
© www.soinside.com 2019 - 2024. All rights reserved.