1

我正在尝试在我的 Symfony 5 应用程序(后端:PHP,前端 JS)中实现 Stripe 以创建订阅(价格取决于选择的数量)

我想要这种情况:

  1. 用户为其订阅选择数量

  2. 它被重定向到结帐页面(我自己创建的,而不是他们自己托管的 Stripe Checkout 实用程序)

  3. 他输入他的付款信息。

  4. 如有必要,SCA 身份验证 (3D SECURE)。

  5. 如果一切正常,我创建订阅。

所以,从第 3 步开始,我尝试了这个:

我的后端端点:

    /**
     * @Route("/paiement/process", name="website_payment_process", options={"expose"=true})
     */
    public function process(Request $request, $stripeSecretKey, ShoppingCart $cart, StripeClient $stripeClient, EntityManagerInterface $manager)
    {
        \Stripe\Stripe::setApiKey($stripeSecretKey);

        $json_obj = json_decode($request->getContent());
        $intent = null;

        try {
            if (isset($json_obj->payment_method_id)) {
                // Create the PaymentIntent
                $intent = \Stripe\PaymentIntent::create([
                    'payment_method' => $json_obj->payment_method_id,
                    'confirmation_method' => 'manual',
                    'confirm' => true,
                    'amount' => $cart->getTotalWithDiscount(false) * 100,
                    'currency' => 'eur',
                    'description' => 'Mon paiement',
                    'setup_future_usage' => 'off_session',
                ]);
            }
            if (isset($json_obj->payment_intent_id)) {
                $intent = \Stripe\PaymentIntent::retrieve(
                    $json_obj->payment_intent_id
                );
                $intent->confirm();
            }
            if ('requires_action' == $intent->status &&
                'use_stripe_sdk' == $intent->next_action->type) {
                // Tell the client to handle the action
                return new JsonResponse([
                    'requires_action' => true,
                    'payment_intent_client_secret' => $intent->client_secret,
                ]);
            } elseif ('succeeded' == $intent->status) {
                // Paiement Stripe accepté
                
                $customer = \Stripe\Customer::create([
                   'description' => 'Abonnement à mon application',
                   'preferred_locales' => ['fr'],
                   'email' => $json_obj->email,
                   'payment_method' => $intent->payment_method,
                ]);

                $stripeCustomer = new StripeCustomer();
                $stripeCustomer->setStripeCustomerId($customer->id);
                $this->getUser()->setStripeCustomer($stripeCustomer);

                $manager->persist($this->getUser());
                $manager->flush();


                // Created the subscription in association with Customer Id
                $stripeClient->createSubscription(
                    $this->getUser(),
                    $cart->getPlan(),
                    true,
                    $cart->getQuantity(),
                    $cart->getCouponId()
                );
                
                return new JsonResponse([
                    'success' => true,
                ]);
            } else {
                return new JsonResponse(['error' => 'Invalid PaymentIntent status'], 500);
            }
        } catch (\Exception $e) {
            // Display error on client
            return new JsonResponse([
                'error' => $e->getMessage(),
            ]);
        }
    }

和 JS:

if($("#payment-form").length !== 0){

    const STRIPE_PUBLIC_KEY = process.env.STRIPE_PUBLIC_KEY;

    // Stripe.setPublishableKey(STRIPE_PUBLIC_KEY);
    var stripe = Stripe(STRIPE_PUBLIC_KEY);
    var processUrl = Routing.generate('website_payment_process');
    var elements = stripe.elements();
    var card = elements.create('card', {
        style: {
            base: {
                iconColor: '#666EE8',
                color: '#31325F',
                lineHeight: '40px',
                fontWeight: 300,
                fontFamily: 'Helvetica Neue',
                fontSize: '15px',

                '::placeholder': {
                    color: '#CFD7E0',
                },
            },
        }
    });
    card.mount('#card-element');

    function setOutcome(result) {
        var errorElement = document.querySelector('.error');
        errorElement.classList.remove('visible');

        if (result.token) {
            $('#stripeToken').val(result.token.id);
            $('#payment-form').submit();
        } else if (result.error) {
            errorElement.textContent = result.error.message;
            errorElement.classList.add('visible');
        }
    }

    card.on('change', function(event) {
        setOutcome(event);
    });

    document.getElementById('payment-form').addEventListener('submit', function(e) {
        e.preventDefault();

        var form = document.getElementById('payment-form');
        stripe.createPaymentMethod('card', card, {
            billing_details: {name: form.querySelector('input[name=cardholder-name]').value}
        }).then(function(result) {
            if (result.error) {
                var errorElement = document.querySelector('.error');
                errorElement.textContent = result.error.message;
                errorElement.classList.add('visible');
            } else {
                $('#buttonPayment').hide();
                $('#spanWaitPayement').show();
                // Otherwise send paymentMethod.id to your server (see Step 2)
                fetch(processUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
                    },
                    body: JSON.stringify({ payment_method_id: result.paymentMethod.id })
                }).then(function(result) {
                    result.json().then(function(json) {
                        handleServerResponse(json);
                    })
                });
            }
        });
    });

    function handleServerResponse(response) {
        if (response.error) {
            $('#buttonPayment').show();
            $('#spanWaitPayement').hide();
            var errorElement = document.querySelector('.error');
            errorElement.textContent = result.error.message;
            errorElement.classList.add('visible');
        } else if (response.requires_action) {
            // Use Stripe.js to handle required card action
            stripe.handleCardAction(
                response.payment_intent_client_secret
            ).then(function(result) {
                if (result.error) {
                    $('#buttonPayment').show();
                    $('#spanWaitPayement').hide();
                    var errorElement = document.querySelector('.error');
                    errorElement.textContent = result.error.message;
                    errorElement.classList.add('visible');
                } else {
                    // The card action has been handled
                    // The PaymentIntent can be confirmed again on the server
                    fetch(processUrl, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
                        },
                        body: JSON.stringify({
                            payment_intent_id: result.paymentIntent.id,
                            email: $('#email').val(),
                        })
                    }).then(function(confirmResult) {
                        return confirmResult.json();
                    }).then(handleServerResponse);
                }
            });
        } else {
            // window.location.replace('/paiement-ok');
        }
    }
}

我设法检索付款信息,使用 SCA 身份验证并进行付款。但是我肯定是错的,因为如果支付成功,就在我创建客户、订阅和关联之前使用的 PaymentIntent 之后。并且不可避免地它不起作用,订阅“不完整”,他再次尝试重新向客户收费

4

2 回答 2

0

您需要添加的是传递off_session: true您的调用以创建订阅。这告诉 Stripe 客户不在场,并提示使用之前通过 3DS 进行的身份验证。有关更多信息,请参阅文档

于 2021-01-15T13:35:21.343 回答
0

您不应该自己创建 PaymentIntent。这将立即向客户收费,这意味着一旦您之后还创建了订阅,他们将被双重收费(除非您让他们试用)。

相反,您应该先翻转并创建订阅,然后确保在需要客户端确认(例如 3D Secure)时完成付款。

您在创建订阅时遇到的错误问题是因为您没有定义用于付款的付款方式。您的代码确实将 PaymentMethod (pm_123) 正确附加到客户,但它没有明确将其设为发票付款的默认值。因此,无论何时创建订阅,它都找不到默认付款方式,最终无法尝试付款。在创建客户时,请确保也invoice_settings[default_payment_method]按照此处记录的方式进行设置。

您的客户创建应如下所示:

$customer = \Stripe\Customer::create([
  'description' => 'Abonnement à mon application',
  'preferred_locales' => ['fr'],
  'email' => $json_obj->email,
  'payment_method' => 'pm_123',
  'invoice_settings' => [
    'default_payment_method' => 'pm_123',
  ],
]);

现在考虑到这一点,您应该遵循的整个流程在本文档中介绍如下所示:

  1. (客户端)使用 Elements 收集卡详细信息并创建 PaymentMethod (pm_123)
  2. (服务器端)创建一个客户并附加 PaymentMethod 并将其设为默认值(上面的代码)
  3. (服务器端)为该客户和正确的价格创建订阅。同时展开latest_invoice.payment_intent确认发票是否已支付或需要3D Secure客户端
  4. (客户端)如果需要,在客户端确认发票的 PaymentIntent 以完成付款并激活订阅。
于 2021-01-16T17:10:26.883 回答