支持3D Secure Card Stripe付款以开始订阅

时间:2019-11-24 19:59:31

标签: laravel stripe-payments stripe-subscriptions

我为应用程序设计的支付模型的想法非常简单:拥有一个具有会员区和特殊功能的(laravel)网站,而会员帐户的费用为每年19.90。我想将Stripe集成到我的注册流程中,以进行付款。付款成功后,我将创建一个订阅,然后每年将自动续订此付款。

到目前为止非常好-我使用Stripe的Guide on how to set up a subscription设法使其正常工作。但是,需要3D安全身份验证的卡仍无法使用,这是必须具备的。

因此,我进一步阅读并使用了PaymentIntentAPI Docs)。但是,当前行为如下:

  • 我创建一个PaymentIntent并将公钥传递给前端
  • 客户输入凭据并提交
  • 3D安全身份验证正确进行,向我返回了payment_method_id
  • 在服务器端,我再次检索了PaymentIntent。它的状态为succeeded,并且付款已在我的Stripe Dashboard上收到。
  • 然后我创建客户对象(使用我从PaymentIntent获得的付款方式),并与该客户创建订阅
  • 该订阅的状态为incomplete,看来该订阅试图再次向客户收费,但由于第二次需要进行3D安全验证,因此该订阅失败了。

所以我的实际问题是:如何创建一个订阅,以某种方式注意到客户已经用我的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 个答案:

答案 0 :(得分:0)

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

因此流程将是:

  1. 创建付款方式
  2. 创建客户(使用付款方式)
  3. 创建订阅(使用客户和付款方式)-还可创建第一张发票
  4. 通过latest_invoice.payment_intent.id从订阅中检索付款意向。在这里,您可以选择是由您还是由Stripe处理。看到这个:How to get PaymentIntent next_action.type = redirect_to_url instead of use_stripe_sdk for Subscription
  5. 允许完成3D安全流程

您的订阅价格不变,因此需要预先付费: https://stripe.com/docs/billing/subscriptions/multiplan#billing-periods-with-multiple-plans

  

按固定间隔收取一定费用的常规计划在每个计费周期开始时进行计费。

答案 1 :(得分:0)

我终于为我的网站运行了一个可行的解决方案。它是这样的:

1-后端:创建SetupIntent

我创建了一个 SetupIntent SetupIntent API Docs),以完全覆盖结帐流程。与PaymentIntentPaymentIntent API Docs)的区别在于,PaymentIntent从收集卡的详细信息,准备付款并有效地将金额转移到帐户上开始,而SetupIntent仅准备卡的收集,而不是尚未执行付款。您会从中获得一个PaymentMethodPaymentMethod API Docs),以后可以使用。

$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。 首先,我们现在需要创建CustomerCustomer API Docs)(如果尚不存在)。在客户身上,我们将通过SetupIntent附加付款方式。然后,我们创建SubscriptionSubscription API Docs),它将从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
]);

现在我们有一个Subscription对象。对于普通卡,状态应为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,他必须首先完成该URL。我让他在新窗口中打开链接,其中显示了由stripe托管的精美发票。在那里,他可以自由地再次确认他的信用卡详细信息,包括3D Secure工作流程。如果他在那里成功,则在您从Stripe检索订阅后,$subscription->status会变为activetrialing

这是一种愚蠢的策略,如果您的实现出现问题,只需将其发送给Stripe即可完成。只要确保提示用户,以防万一他必须两次确认他的卡,它不会被收取两次费用,而只会被收取一次!

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