支持3D安全卡片的Stripe支付以开始订阅。

4
我的应用程序的付款模型想法非常简单:拥有一个(laravel)网站,具有会员区域和一些特殊功能,其中会员帐户费用为19.90美元/年。我想将Stripe集成到我的注册流程中,以允许发生付款。当付款成功时,我创建一个订阅,然后每年自动续订此付款。
到目前为止很好 - 我已经使用Stripe的设置订阅的指南使其工作。但是,需要3D Secure身份验证的卡尚未工作,这是必须的。
因此,我进一步阅读并使用了PaymentIntentAPI文档)。但是,当前行为如下:
  • 我创建了一个PaymentIntent,并将公钥传递给前端
  • 客户输入凭据并提交
  • 3D安全验证正确进行,向我返回payment_method_id
  • 在服务器端,我再次检索PaymentIntent。它的状态为succeeded,并且付款已在我的Stripe Dashboard上收到。
  • 然后,我创建客户对象(使用从PaymentIntent获取的付款方式),然后使用该客户创建订阅
  • 订阅的状态为incomplete,似乎订阅尝试再次向客户收费,但由于第二次需要进行3D Secure验证而失败。

所以我的实际问题是:如何创建一个订阅,使其以某种方式注意到客户已经使用我的PaymentIntent和我传递给它的PaymentMethod进行了支付?

一些代码

创建PaymentIntent并将其传递给前端

\Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
$intent = \Stripe\PaymentIntent::create([
   'amount' => '1990',
   'currency' => 'chf',
]);
$request->session()->put('stripePaymentIntentId',$intent->id);
return view('payment.checkout')->with('intentClientSecret',$intent->client_secret);

点击“购买”时的前端结账

// I have stripe elements (the card input field) ready and working
// using the variable "card". The Stripe instance is saved in "stripe".
// Using "confirmCardPayment", the 3DS authentication is performed successfully.
stripe.confirmCardPayment(intentClientSecret,{
    payment_method: {card: mycard},
    setup_future_usage: 'off_session'
}).then(function(result) {
    $('#card-errors').text(result.error ? result.error.message : '');
    if (!result.error) {
        submitMyFormToBackend(result.paymentIntent.payment_method);
    }
    else {
        unlockPaymentForm();
    }
});

提交后的后端
// Get the PaymentMethod id from the frontend that was submitted
$payment_method_id = $request->get('stripePaymentMethodId');
// Get the PaymentIntent id which we created in the beginning
$payment_intent_id = $request->session()->get('stripePaymentIntentId');
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
// Get the Laravel User
$user = auth()->user();

// Firstly load Payment Intent to have this failing first if anything is not right
$intent = \Stripe\PaymentIntent::retrieve($payment_intent_id);
if ($intent instanceof \Stripe\PaymentIntent) {
    // PaymentIntent loaded successfully.

    if ($intent->status == 'succeeded') {

        // The intent succeeded and at this point I believe the money
        // has already been transferred to my account, so it's paid.
        // Setting up the user with the paymentMethod given from the frontend (from
        // the 3DS confirmation).
        $customer = \Stripe\Customer::create([
            'payment_method' => $payment_method_id,
            'email' => $user->email,
            'invoice_settings' => [
                'default_payment_method' => $payment_method_id,
            ],
        ]);

        $stripeSub = \Stripe\Subscription::create([
            'customer' => $customer->id,
            'items' => [
                [
                    'plan' => env('STRIPE_PLAN_ID'),
                ]
            ],
            'collection_method' => 'charge_automatically',
            'off_session' => false,
        ]);

        // If the state of the subscription would be "active" or "trialing", we would be fine
        // (depends on the trial settings on the plan), but both would be ok.
        if (in_array($stripeSub->status,['active','trialing'])) {
            return "SUCCESS";
        }

        // HOWEVER the state that I get here is "incomplete", thus it's an error.
        else {
            return "ERROR";
        }
    }
}
2个回答

5

我终于为我的网站找到了一个可行的解决方案,具体如下:

1 - 后端:创建 SetupIntent

我创建了一个SetupIntentSetupIntent API 文档)来完全覆盖结账流程。与PaymentIntentPaymentIntent API 文档)不同的是,PaymentIntent 从收集卡片详细信息、准备支付,到实际将金额转账到帐户,而 SetupIntent 只准备收集银行卡信息,但尚未执行付款。您将从中获得一个PaymentMethodPaymentMethod API 文档),稍后可以使用它。

$intent = SetupIntent::create([
    'payment_method_types' => ['card'],
]);

接着,我将$intent->client_secret密钥传递给了客户端JavaScript。

2 - 前端:使用 Elements 收集卡片信息

在前端,我放置了 Stripe 的卡片元素以收集卡片信息。

var stripe = Stripe(your_stripe_public_key);
var elements = stripe.elements();
var style = { /* my custom style definitions */ };
var card = elements.create('card',{style:style});
card.mount('.my-cards-element-container');

// Add live error message listener 
card.addEventListener('change',function(event) {
    $('.my-card-errors-container').text(event.error ? event.error.message : '');
}

// Add payment button listener
$('.my-payment-submit-button').on('click',function() {
    // Ensure to lock the Payment Form while performing async actions
    lockMyPaymentForm();
    // Confirm the setup without charging it yet thanks to the SetupIntent.
    // With 3D Secure 2 cards, this will trigger the confirmation window.
    // With 3D Secure cards, this will not trigger a confirmation.
    stripe.confirmCardSetup(setup_intent_client_secret, {
        payment_method: {card: card} // <- the latter is the card object variable
    }).then(function(result) {
        $('.my-card-errors-container').text(event.error ? event.error.message : '');
        if (!result.error) {
            submitPaymentMethodIdToBackend(result.setupIntent.payment_method);
        }
        else {
            // There was an error so unlock the payment form again.
            unlockMyPaymentForm();
        }
    });
}

function lockMyPaymentForm() {
    $('.my-payment-submit-button').addClass('disabled'); // From Bootstrap
    // Get the card element here and disable it
    // This variable is not global so this is just sample code that does not work.
    card.update({disabled: true});
}

function unlockMyPaymentForm() {
    $('.my-payment-submit-button').removeClass('disabled'); // From Bootstrap
    // Get the card element here and enable it again
    // This variable is not global so this is just sample code that does not work.
    card.update({disabled: false});
}

3 - 后端:创建客户和订阅

在后端,我收到了从前端提交的$payment_method_id。 首先,如果尚未存在,则需要创建客户顾客API文档)。我们将在客户上附加来自SetupIntent的付款方式。然后,我们创建订阅订阅API文档),它将启动来自SetupIntent的收费。

$customer = \Stripe\Customer::create([
    'email' => $user->email, // A field from my previously registered laravel user
]);

$paymentMethod = \Stripe\PaymentMethod::retrieve($payment_method_id);

$paymentMethod->attach([
    'customer' => $customer->id,
]);

$customer = \Stripe\Customer::update($customer->id,[
    'invoice_settings' => [
        'default_payment_method' => $paymentMethod->id,
    ],
]);

$subscription = \Stripe\Subscription::create([
    'customer' => $customer->id,
    'items' => [
        [
            'plan' => 'MY_STRIPE_PLAN_ID',
        ],
    ],
    'off_session' => TRUE, //for use when the subscription renews
]);

现在我们有一个订阅对象。对于普通卡,状态应该是activetrialing,具体取决于订阅上的试用天数设置。但是当处理3D Secure测试卡时,我发现订阅仍处于incomplete状态。根据我的Stripe支持联系人所说,这也可能是由于尚未完全工作的3D Secure测试卡的问题。然而,我认为在生产环境中也可能会出现某些类型的卡片导致此问题,因此我们必须处理它。
对于状态为incomplete的订阅,您可以像这样从$subscription->latest_invoice检索最新发票:
$invoice = \Stripe\Invoice::retrieve($subscription->latest_invoice); 

在您的发票对象上,您会找到一个status和一个hosted_invoice_url。当status仍然是open时,我现在向用户展示他必须先完成的托管发票的URL。我让他在新窗口中打开链接,显示Stripe托管的漂亮发票。在那里,他可以自由地再次确认他的信用卡详细信息,包括3D安全工作流程。如果他在那里成功,重新从Stripe检索订阅后,$subscription-> status 会更改为active或trialing。

这是一种类似于防弊策略,如果您的实现出了问题,只需将其发送到Stripe以完成它。只需确保向用户提示,如果他需要确认两次他的卡片,那么付款只会被扣取一次,而不是两次!

我无法创建@snieguu解决方案的可行版本,因为我想使用Elements而不是单独收集信用卡详细信息,然后自己创建PaymentMethod。


1
@Jeroen,非常棒的教程!我也遇到了同样的问题。不确定,但是如果我将“trial_end”属性设置为未来的某个日期,“incomplete”状态是否会更改为“trialing”。不确定订阅是否会被无忧扣款。 - Arnis Juraga
你好,非常棒的教程。但是你将如何处理无效的信用卡或者没有足够资金的信用卡呢?在这种情况下,很遗憾,没有进行任何验证,订阅仍然会被创建(虽然它可能在Stripe上不起作用,但是在你的应用程序中编写一些代码来验证卡片是否有效并开始创建订阅过程是很好的),但我们该怎么做呢? - eronn
我刚刚尝试了一下使用测试卡号,该卡号默认情况下会被拒绝(我认为未覆盖的卡片或类似的卡片会被拒绝)。我使用的是卡号 4000 0084 0000 1629(请参见 https://stripe.com/docs/testing#regulatory-cards),当我调用 stripe.confirmCardSetup 时,它返回了一个错误,所以对我来说这很好。 - Florian Müller
致命错误:未捕获(状态404)(请求req_paXIpydp6W3YIs)无此付款方式:'pi_1IWV3TFkBCqKGjksZH6YdohK',位于C:\ xampp \ htdocs \ stripe \ vendor \ stripe \ stripe-php \ lib \ Exception \ ApiErrorException.php的第38行。 - Fernando Torres
致命错误:未捕获(状态400)(请求req_MuoZL7StsEhJMX)。此PaymentMethod以前曾被使用,但未附加到客户或已从客户中分离,并且不能再次使用。位于C:\xampp\htdocs\stripe\vendor\stripe\stripe-php\lib\Exception\ApiErrorException.php的第38行引发了异常。 - Fernando Torres

1

您是否考虑过相反的方法,即支付意图(也是第一个)将由订阅生成-而不是手动创建?

因此,流程将如下:

  1. 创建付款方式
  2. 创建客户(使用付款方式)
  3. 创建订阅(使用客户和付款方式)-这也会创建第一张发票
  4. 通过latest_invoice.payment_intent.id从订阅中检索支付意图。在这里,您可以选择由您或Stripe处理。请参见:如何获取PaymentIntent next_action.type = redirect_to_url而不是use_stripe_sdk用于订阅
  5. 允许完成3D安全流程

您的订阅有固定的价格,因此将提前收费: https://stripe.com/docs/billing/subscriptions/multiplan#billing-periods-with-multiple-plans

按间隔收取固定金额的传统计划在每个结算周期开始时结算。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接