온라인 구매 후 매장 수령: Bonjour Meal - 3부 - 결제 대행업체와 통합

1. 소개

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

최종 업데이트: 2021년 9월 13일

지급을 받으세요.

Business Messages에서 결제를 수집하면 대화형 플랫폼 내에서 완전히 새로운 비즈니스 기회의 가능성이 열립니다. 잠재고객이 자세히 알아보고 싶은 제품에 대해 문의한다고 상상해 보세요. 사용자가 질문에 답하면 대화 내에서 결제 게이트웨이를 제공하여 거래를 체결할 수 있습니다.

fe0c6754fb69d708.png

우수한 결제 환경을 만드는 요소는 무엇인가요?

좋은 결제 환경에서는 사용자가 익숙한 방식으로 결제할 수 있습니다.

사용자는 결제 방식을 선호하며 다른 결제 수단은 전 세계 다른 지역에 비해 더 일반적입니다. Business Messages를 사용하면 둘 이상의 결제 프로세서를 통합하여 사용자에게 가장 편리한 방법을 제공할 수 있습니다.

사용자가 결제 절차를 완료하면 결제가 정상적으로 처리되었음을 사용자에게 알려야 합니다. 대부분의 결제 대행업체에는 결제 과정이 완료되면 내가 선택한 URL로 HTTP 요청을 전송하는 성공 또는 실패 콜백이 포함됩니다.

빌드할 항목

Codelab 시리즈 이전 섹션에서는 항목 카탈로그를 제공하도록 Bonjour Meal 에이전트를 확장하고, 사용자가 항목을 추가 및 삭제할 수 있는 장바구니를 만들었고, 장바구니의 총 가격을 계산했습니다. 이 섹션에서는 상담사가 장바구니 콘텐츠를 바탕으로 결제를 처리할 수 있도록 에이전트를 더 확장합니다.

이 Codelab에서 앱은 다음 기능을 수행합니다.

  • Stripe 결제 게이트웨이와 통합
  • 사용자가 장바구니 가격에 따라 결제 절차를 완료하도록 허용
  • 대화형 표면에 알림을 보내 사용자에게 결제 상태를 알립니다.

ba08a4d2f8c09c0e.png

실행할 작업

  • Stripe 결제 프로세서와 통합
  • Stripe에 요청을 보내 결제 세션을 시작합니다.
  • Stripe의 결제 성공 또는 실패 응답을 처리합니다.

필요한 항목

  • Business Messages에 사용할 수 있도록 등록 및 승인된 GCP 프로젝트
  • 개발자 사이트에서 방법 안내 확인
  • 버전이 5 이상인 Android 기기 또는 Google 지도 앱이 설치된 iOS 기기
  • 웹 애플리케이션 프로그래밍 경험
  • 인터넷 연결

2. 종속 항목 추가

requirements.txt 업데이트

Stripe 결제 프로세서와 통합될 것이므로 Stripe Python 클라이언트 라이브러리를 사용할 수 있습니다. 버전이 없는 requirements.txt에 stripe를 추가하여 최신 버전의 종속 항목을 가져옵니다.

이는 Google Cloud App Engine Python 런타임에 스트라이프 Python 모듈을 포함하는 데 필요합니다.

requirements.txt

...
stripe
...

bopis/views.py 준비

bopis/views.py 상단에서 django.shortcuts에서 render, django.http에서 JsonResponse를 가져옵니다. 또한 Stripe Python 클라이언트 라이브러리 호출을 지원하기 위해 stripe를 가져와야 합니다.

...
from django.shortcuts import render
from django.http import JsonResponse
import stripe
...

3. Stripe 작업

Stripe.com에서 계정 만들기

이 Codelab에서는 Stripe를 사용했지만 웹 통합을 지원하는 모든 프로세서와 통합할 수 있습니다. stripe.com에서 계정을 만듭니다. Google에서는 테스트 및 교육 목적으로 이 프로필을 사용하여 타사 결제 대행업체와 직접 통합하는 방법을 알아봅니다.

6731d123c56feb67.png

계정을 만들고 로그인하면 다음과 같은 대시보드가 표시됩니다.

6d9d165d2d1fbb8c.png

'테스트 모드'로 작동 중인지 확인하고 위 스크린샷에 설명된 개발자 버튼을 클릭하여 API 키를 찾습니다. 게시 가능한 키보안 비밀 키라는 두 가지 API 키 세트가 표시됩니다. Stripe에서 결제 거래를 용이하게 하려면 이 두 키가 모두 필요합니다.

bopis/views.py 업데이트

애플리케이션에는 두 키 세트가 모두 필요하므로 views.py에서 업데이트해야 합니다.

보안 비밀 키를tripe.api_key 속성에서 직접 설정하고 Stripe 개발자 대시보드에 있는 보안 비밀 키의 값을 할당할 수 있습니다. 그런 다음 STRIPE_PUBLIC_KEY라는 전역 변수를 만들고 게시 가능한 키로 설정합니다.

또한 Stripe는 개발자가 관리하는 웹페이지로 다시 리디렉션해야 하므로 애플리케이션의 전역 연결 가능 도메인을 포함하는 추가 전역 변수를 만듭니다.

수정을 마치면 다음과 같이 표시됩니다.

stripe.api_key = 'sk_test_abcde-12345'
STRIPE_PUBLIC_KEY = 'pk_test_edcba-54321'
YOUR_DOMAIN = 'https://<GCP_PROJECT_ID>.appspot.com'

Stripe 설정 시 필요한 작업은 이것뿐입니다.

4. 결제 기능

장바구니 총 가격 함수 업데이트

현재 send_shopping_cart_total_price 함수는 장바구니 가격을 지정하는 메시지만 보냅니다. 결제 페이지에 대한 URL을 열기 위해 권장 조치를 추가해 보겠습니다.

def send_shopping_cart_total_price(conversation_id):
  """Sends shopping cart price to the user through Business Messages.

  Args:
    conversation_id (str): The unique id for this user and agent.
  """
  cart_price = get_cart_price(conversation_id)

  message_obj = BusinessMessagesMessage(
      messageId=str(uuid.uuid4().int),
      representative=BOT_REPRESENTATIVE,
      text=f'Your cart\'s total price is ${cart_price}.',
      suggestions=[
          BusinessMessagesSuggestion(
              action=BusinessMessagesSuggestedAction(
                  text='Checkout',
                  postbackData='checkout',
                  openUrlAction=BusinessMessagesOpenUrlAction(
                      url=f'{YOUR_DOMAIN}/checkout/{conversation_id}'))),
      ]
    )

  send_message(message_obj, conversation_id)

사용자가 이 추천 작업을 탭하면 총 가격 및 Stripe로 결제를 시작하는 버튼이 표시된 웹페이지로 이동합니다.

이 흐름을 지원하는 간단한 웹페이지를 만들어 보겠습니다.

프로젝트 소스 코드에서 bopis라는 디렉터리를 찾습니다. bopis 내에 templates라는 새 디렉터리를 만들고 템플릿 내에 bopis라는 또 다른 디렉터리를 만듭니다. 이것은 템플릿 디렉터리 내에서 앱 이름을 지정하는 장고 설계 패턴입니다. Bazel 앱 간의 템플릿 혼동을 줄이는 데 도움이 됩니다.

이제 bopis/templates/bopis/에 경로가 포함된 디렉터리가 있습니다. 이 디렉터리에 HTML 파일을 만들어 웹페이지를 제공할 수 있습니다. Vulkan에서는 이를 브라우저에 렌더링되는 템플릿이라고 합니다. checkout.html(으)로 시작해 보세요.

이 디렉터리에 checkout.html 파일을 만듭니다. 다음 코드 스니펫은 결제 버튼과 장바구니의 가격을 표시합니다. Stripe 결제를 시작하는 자바스크립트도 포함되어 있습니다.

{% load static %}

<!DOCTYPE html>
<html>
  <head>
    <title>Purchase from Bonjour Meal</title>

    <script src="https://js.stripe.com/v3/"></script>
    <style>
      .description{
        font-size: 4em;
      }
      button {
        color: red;
        padding: 40px;
        font-size: 4em;
      }
    </style>
  </head>
  <body>
    <section>
      <img
        src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
        alt="Bonjour Meal Restaurant"
      />
      <div class="product">
        <div class="description">
          <h3>Your Bonjour Meal Total</h3>
          <h5>${{cart_price}}</h5>
        </div>
      </div>
      <button type="button" id="checkout-button">Checkout</button>
    </section>
  </body>
  <script type="text/javascript">
    // Create an instance of the Stripe object with your publishable API key
    var stripe = Stripe("{{stripe_public_key}}");
    var checkoutButton = document.getElementById("checkout-button");

    checkoutButton.addEventListener("click", function () {
      fetch("/create-checkout-session/{{conversation_id}}", {
        method: "POST",
      })
        .then(function (response) {
          return response.json();
        })
        .then(function (session) {
          return stripe.redirectToCheckout({ sessionId: session.id });
        })
        .then(function (result) {
          // If redirectToCheckout fails due to a browser or network
          // error, you should display the localized error message to your
          // customer using error.message.
          if (result.error) {
            alert(result.error.message);
          }
        })
        .catch(function (error) {
          console.error("Error:", error);
        });
    });
  </script>
</html>

URL을 요청하면 이 웹페이지로 연결되는 경로가 필요합니다. 결제 추천 액션에 openUrlAction 값이 {YOUR_DOMAIN}/checkout/{conversation_id}(으)로 설정되어 있습니다. https://<GCP-Project-ID>.appspot.com/checkout/abc123-cba321-abc123-cba321와 같은 형식으로 변환됩니다. 이 경로를 만들기 전에 HTML 템플릿 내에 있는 자바스크립트를 살펴보겠습니다.

...
  <script type="text/javascript">
    // Create an instance of the Stripe object with your publishable API key
    var stripe = Stripe("{{stripe_public_key}}");
    var checkoutButton = document.getElementById("checkout-button");

    checkoutButton.addEventListener("click", function () {
      fetch("/create-checkout-session/{{conversation_id}}", {
        method: "POST",
      })
        .then(function (response) {
          return response.json();
        })
        .then(function (session) {
          return stripe.redirectToCheckout({ sessionId: session.id });
        })
        .then(function (result) {
          // If redirectToCheckout fails due to a browser or network
          // error, you should display the localized error message to your
          // customer using error.message.
          if (result.error) {
            alert(result.error.message);
          }
        })
        .catch(function (error) {
          console.error("Error:", error);
        });
    });
  </script>
...

위의 코드 스니펫을 함께 살펴보겠습니다.

  1. 먼저 뷰 함수, 다른 장고 패러다임의 컨텍스트를 통해 전달되는 공개 키를 사용하여 Stripe 항목을 만듭니다.
  2. 그런 다음 스니펫은 ID가 checkout-button인 페이지에서 요소를 찾습니다.
  3. 이벤트 리스너가 이 요소에 추가됩니다.

이 이벤트 리스너는 사용자가 이 버튼을 클릭하거나 탭하면 URL을 통해 지정한 웹 서버에 대한 POST 요청을 시작합니다. {YOUR_DOMAIN}/create-checkout-session/{conversation_id}.

아래 스니펫에서 웹 서버 로직을 확인할 수 있습니다. ID ID가 checkout-button인 버튼을 탭하면 장바구니 가격을 지정하는 Stripe API를 사용하여 만든 Stripe 세션 ID가 반환될 것으로 예상됩니다.

서버가 유효한 세션 ID를 생성할 수 있으면 애플리케이션 로직에서 사용자를 Stripe 결제 페이지로 리디렉션합니다. 그렇지 않은 경우 문제가 있음을 표준 자바스크립트 메시지로 사용자에게 알립니다.

먼저 urlpatterns 배열에 새 경로를 추가하여 결제 페이지를 지원하고 세션 ID를 생성해 보겠습니다. urls.py의 urlpatterns 배열에 다음을 추가합니다.

... 
path('checkout/<str:conversation_id>', bopis_views.payment_checkout),
path('create-checkout-session/<str:conversation_id>', bopis_views.create_checkout_session),
...

그런 다음 views.py에서 뷰 함수를 생성하여 Checkout.html 템플릿을 반환하고 Stripe 결제 세션을 생성합니다.

... 

def payment_checkout(request, conversation_id):
  """Sends the user to a payment confirmation page before the payment portal.

  Args:
    request (HttpRequest): Incoming Django request object
    conversation_id (str): The unique id for this user and agent.

  Returns:
    Obj (HttpResponse): Returns an HTTPResponse to the browser
  """

  cart_price = get_cart_price(conversation_id)
  context = {'conversation_id': conversation_id,
             'stripe_public_key': STRIPE_PUBLIC_KEY,
             'cart_price': cart_price
            }
  return render(request, 'bopis/checkout.html', context)


@csrf_exempt
def create_checkout_session(request, conversation_id):
  """Creates a Stripe session to start a payment from the conversation.

  Args:
    request (HttpRequest): Incoming Django request object
    conversation_id (str): The unique id for this user and agent.

  Returns:
    Obj (HttpResponse): Returns an HTTPResponse to the browser
  """
  cart_price = get_cart_price(conversation_id)
  try:
    checkout_session = stripe.checkout.Session.create(
        payment_method_types=['card'],
        line_items=[
            {
                'price_data': {
                    'currency': 'usd',
                    'unit_amount': int(cart_price*100),
                    'product_data': {
                        'name': 'Bonjour Meal Checkout',
                        'images': ['https://storage.googleapis.com/bonjour-rail.appspot.com/apple-walnut-salad.png'],
                    },
                },
                'quantity': 1,
            },
        ],
        mode='payment',
        success_url=YOUR_DOMAIN + '/success/' + conversation_id,
        cancel_url=YOUR_DOMAIN + '/cancel/' + conversation_id,
    )

    return JsonResponse({
        'id': checkout_session.id
    })

  except Exception as e:
    # Handle exceptions according to your payment processor's documentation
    # https://stripe.com/docs/api/errors/handling?lang=python
    return HttpResponse(e)

...

두 함수 모두 나와의 대화를 통해 장바구니를 사용자에게 연결한 다음 Stripe에서 사용자에게 청구할 가격을 결정합니다.

이 두 가지 방법은 결제 수단의 전반부입니다. 이를 배포하고 환경을 테스트하면 Stripe 개발자 문서에서 Visa 결제 테스트에 설명된 대로 테스트 신용카드로 결제를 완료할 수 있는 Stripe 결제 양식이 표시됩니다.

이 흐름의 후반부에서는 Stripe로부터 사용자의 결제와 관련된 응답을 받으면 사용자를 대화로 다시 돌아오게 합니다.

5. 스트라이프 응답

사용자가 결제 과정에 참여할 때 결제를 완료하거나 완료할 수 없습니다. create_checkout_session 함수에서 success_urlcancel_url을 정의했습니다. Stripe는 결제 상태에 따라 이 두 URL 중 하나로 리디렉션됩니다. urls.py에서 이러한 두 경로를 정의한 다음 두 개의 뷰 함수를 bopis/views.py에 추가하여 가능한 두 흐름을 지원하도록 하겠습니다.

다음 행을 urls.py 파일에 추가합니다.

... 
    path('success/<str:conversation_id>', bopis_views.payment_success),
    path('cancel/<str:conversation_id>', bopis_views.payment_cancel),
...

상응하는 뷰는 다음과 같습니다.

... 

def payment_success(request, conversation_id):
  """Sends a notification to the user prompting them back into the conversation.

  Args:
    request (HttpRequest): Incoming Django request object
    conversation_id (str): The unique id for this user and agent.

  Returns:
    Obj (HttpResponse): Returns an HTTPResponse to the browser
  """
  message_obj = BusinessMessagesMessage(
      messageId=str(uuid.uuid4().int),
      representative=BOT_REPRESENTATIVE,
      suggestions=[
          BusinessMessagesSuggestion(
              reply=BusinessMessagesSuggestedReply(
                  text='Check on order', postbackData='check-order')),
      ],
      text='Awesome it looks like we\'ve received your payment.')

  send_message(message_obj, conversation_id)

  return render(request, 'bopis/success.html')


def payment_cancel(request, conversation_id):
  """Sends a notification to the user prompting them back into the conversation.

  Args:
    request (HttpRequest): Incoming Django request object
    conversation_id (str): The unique id for this user and agent.

  Returns:
    Obj (HttpResponse): Returns an HTTPResponse to the browser
  """
  message_obj = BusinessMessagesMessage(
      messageId=str(uuid.uuid4().int),
      representative=BOT_REPRESENTATIVE,
      suggestions=[
          BusinessMessagesSuggestion(
              action=BusinessMessagesSuggestedAction(
                  text='Checkout',
                  postbackData='checkout',
                  openUrlAction=BusinessMessagesOpenUrlAction(
                      url=f'{YOUR_DOMAIN}/checkout/{conversation_id}'))),
      ],
      text='It looks like there was a problem with checkout. Try again?')

  send_message(message_obj, conversation_id)

  return render(request, 'bopis/cancel.html')

...

Stripe는 DOMAIN 상수에 지정한 대로 도메인으로 다시 리디렉션됩니다. 즉, 템플릿을 통해 HTML 응답을 렌더링해야 합니다. 그렇지 않으면 웹사이트가 매우 기본적인 모습으로 나타납니다. bopis/templates/bopis/ 디렉터리에 Checkout.html과 함께 간단한 HTML 파일 2개를 만듭니다.

bm-django-echo-bot/bopis/ templates/bopis/success.html

{% load static %}

<html>
<head>
  <title>Business Messages Payment Integration Sample!</title>
  <style>
    p{
      font-size: 4em;
    }
  </style>
</head>
<body>
  <section>

    <p>
      Checkout succeeded - We appreciate your business!
      <br/><br/>
      For support related questions, please email
      <a href="mailto:bm-support@google.com">bm-support@google.com</a>.

    </p>
  </section>
</body>
</html>

bm-django-echo-bot/bopis/ templates/bopis/cancel.html

{% load static %}

<html>
<head>
  <title>Checkout canceled</title>
  <style>
    p{
      font-size: 4em;
    }
    </style>
</head>
<body>
  <section>
    <p>Checkout canceled - Forgot to add something to your cart? Shop around then come back to pay!</p>
  </section>
</body>
</html>

이 두 템플릿을 사용하면 Stripe 통합으로 결제 절차를 완료하는 사용자가 적절한 URL로 리디렉션되고 해당 템플릿이 표시됩니다. 또한 Business Messages를 통해 대화로 돌아갈 수 있는 메시지가 전송됩니다.

6. 수익금을 받으세요!

축하합니다. Business Messages 에이전트에 결제 프로세서가 통합되었습니다.

이 시리즈에서는 Google Cloud App Engine에 웹 애플리케이션을 배포하고, 비즈니스 커뮤니케이션 개발자 콘솔에 웹훅을 설정하고, 정적 데이터베이스를 통한 인벤토리 조회를 지원하도록 애플리케이션을 확장하고, Google Datastore를 사용하여 장바구니를 만들었습니다. 이 시리즈의 마지막 부분에서 웹 통합과 이 경험을 지원하는 결제 프로세서인 Stripe와 통합했습니다. 이제 다른 결제 대행업체 등과 통합할 수 있습니다.

d6d80cf9c9fc621.png 44db8d6441dce4c5.png

다음 단계

준비되었으면 다음 주제를 체크아웃해서 Business Messages에서 수행할 수 있는 복잡한 상호작용에 대해 자세히 알아보세요.

참조 문서