Совершение покупки с помощью Checkout, или «Подождите, за что я заплатил?».

Представьте, что вы находитесь в середине процесса настройки интеграции платежей. Вы внедрили Stripe Checkout, запустили веб-крючки и даже установили приложение Slack, которое будет сообщать вам о поступлении денег.

Далее вам нужно фактически предоставить товар или услугу, которую вы продаете своим клиентам. «Не проблема!» — думаете вы, не подозревая, что сейчас вам докажут обратное. Вы просто добавите некоторую бизнес-логику в свой бэкенд, когда получите событие checkout.session.completed webhook. Вы пробуете это в тестовом режиме и получаете полезную нагрузку, не похожую на следующую:

{
 "object": {
  "id": "cs_test_a16Dn1Ja9hTBizgcJ9pWXM5xnRMwivCYDVrT55teciF0mc3vLCUcy6uO99",
  "object": "checkout.session",
  "after_expiration": null,
  "allow_promotion_codes": null,
  "amount_subtotal": 3000,
  "amount_total": 3000,
  "automatic_tax": {
   "enabled": false,
   "status": null
  },
  "billing_address_collection": null,
  "cancel_url": "https://example.com/cancel",
  "client_reference_id": null,
  "consent": null,
  "consent_collection": null,
  "currency": "usd",
  "customer": "cus_M5Q7YRXNqZrFtu",
  "customer_creation": "always",
  "customer_details": {
   "address": {
    "city": null,
    "country": null,
    "line1": null,
    "line2": null,
    "postal_code": null,
    "state": null
   },
   "email": "stripe@example.com",
   "name": null,
   "phone": null,
   "tax_exempt": "none",
   "tax_ids": [
   ]
  },
  "customer_email": null,
  "expires_at": 1658319119,
  "livemode": false,
  "locale": null,
  "metadata": {
  },
  "mode": "payment",
  "payment_intent": "pi_3LNFHPGUcADgqoEM2rxLo91k",
  "payment_link": null,
  "payment_method_options": {
  },
  "payment_method_types": [
   "card"
  ],
  "payment_status": "paid",
  "phone_number_collection": {
   "enabled": false
  },
  "recovered_from": null,
  "setup_intent": null,
  "shipping": null,
  "shipping_address_collection": null,
  "shipping_options": [
  ],
  "shipping_rate": null,
  "status": "complete",
  "submit_type": null,
  "subscription": null,
  "success_url": "https://example.com/success",
  "total_details": {
   "amount_discount": 0,
   "amount_shipping": 0,
   "amount_tax": 0
  },
  "url": null
 }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Из этих данных вы можете узнать, кто и сколько заплатил, но что на самом деле купил пользователь? Как узнать, что отправлять, если вы продаете физические товары, или что предоставлять, если ваши товары цифровые?

Это причуда, которая ставит в тупик многих людей, когда они доходят до этого этапа. Вы, вероятно, помните, что при создании сеанса оформления заказа вы указывали line_items — это поле, в котором вы указываете, что именно покупает пользователь, либо предоставляя идентификатор цены, либо создавая цену ad-hoc.

Это поле не включается по умолчанию при получении сеанса оформления заказа, а также не входит в полезную нагрузку события webhook. Вместо этого вам нужно получить Checkout Session из Stripe API, расширив при этом необходимые вам поля. Расширение — это процесс запроса дополнительных данных или объектов из одного вызова Stripe API. С его помощью вы можете, например, получить и подписку, и связанный с ней объект Customer одним вызовом API, а не двумя.

⚠️ Подсказка: свойства, которые можно расширять, отмечены как таковые в справке API. Вы можете узнать больше о расширении в нашей серии видео.

Вот пример с использованием Node и Express, как это будет выглядеть в коде события webhook:

app.post('/webhook', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const endpointSecret = process.env.WEBHOOK_SECRET;

  let event;

  // Verify the webhook signature
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.log(`Webhook error: ${err.message}`);
    return res.status(400).send(`Webhook error: ${err.message}`);
  }

  // Acknowledge that the event was received _before_ doing our business logic
  // That way if something goes wrong we won't get any webhook event retries
  res.json({ received: true });

  switch (event.type) {
    case 'checkout.session.completed':

      // Retrieve the session, expanding the line items
      const session = await stripe.checkout.sessions.retrieve(
        event.data.object.id,
        {
          expand: ['line_items'],
        }
      );

      const items = session.line_items.data.map((item) => {
        return item.description;
      });

      console.log(`
        Items purchased: ${items.join(' ')}
        Total amount: ${session.amount_total}
      `);

      break;    
  }
});
Вход в полноэкранный режим Выход из полноэкранного режима

Вышеприведенный пример получит ту же сессию оформления заказа, но попросит API включить полный объект line_items, который иначе вы бы не получили. Затем мы выводим описание каждого купленного товара и общую сумму, которую заплатил покупатель.

Такой подход может показаться тупым (почему бы просто не включить line_items в полезную нагрузку?), но на самом деле у такого подхода есть веские причины и преимущества.

Латентность

Правда в том, что получение полного списка позиций и возвращение их в полезную нагрузку события webhook требует больших вычислительных затрат. Это особенно актуально, если у вас много позиций для одной сессии оформления заказа. В сочетании с тем фактом, что многие пользователи Stripe не используют содержимое line_items, добавление их в каждую полезную нагрузку значительно увеличит задержку события webhook. Поэтому Stripe решила сделать это свойство опциональным, чтобы сохранить скорость работы API для всех.

Убедитесь, что у вас всегда есть последний актуальный объект

Представьте себе сценарий, в котором ваш клиент создает новую подписку, а вы прослушиваете следующие события webhook:

customer.subscription.created
invoice.created
invoice.paid
Вход в полноэкранный режим Выход из полноэкранного режима

Где на каждом шаге вы хотите выполнить некоторую бизнес-логику перед следующим шагом (например, обновление статуса в вашей базе данных).

Затем, когда вы тестируете свою интеграцию, вы получаете события в таком порядке:

invoice.created
invoice.paid
customer.subscription.created
Вход в полноэкранный режим Выход из полноэкранного режима

Чего ждать? Как счет может быть создан и оплачен до создания подписки?

Ну, это не так. Последовательность веб-крючков, к сожалению, не может быть доверена из-за того, как работает интернет. Хотя события могут быть отправлены по порядку из Stripe, нет никакой гарантии, что они будут получены по порядку (я виню интернет гремлинов). Это особенно верно для событий, которые генерируются и отправляются в быстрой последовательности, например, события, связанные с созданием новой подписки.

Если ваша бизнес-логика полагается на то, что эти события происходят по порядку, а объекты Stripe находятся в разных состояниях, могут произойти неприятные вещи. Вы можете полностью смягчить эту проблему, всегда получая соответствующий объект перед внесением каких-либо изменений. Таким образом, вы гарантируете, что у вас всегда будет самый актуальный объект, который отражает то, что Stripe имеет на своей стороне. Хотя это означает, что вам придется сделать один дополнительный вызов API, это также означает, что у вас никогда не будет устаревших данных или страдать от гнева интернет гремлина.

Подведение итогов

Это были некоторые советы по проведению сверки покупок и некоторые лучшие практики использования webhook. Я что-то упустил или у вас есть вопросы? Дайте мне знать в комментариях ниже или в Twitter!

Об авторе

Пол Асджес — представитель разработчиков в Stripe, где он пишет, кодирует и ведет ежемесячную серию Q&A, общаясь с разработчиками. Вне работы он любит варить пиво, готовить билтонг и проигрывать своему сыну в Mario Kart.

Оцените статью
devanswers.ru
Добавить комментарий