اشترِ خدمة استلام الطلب على الإنترنت من المتجر: وجبة Bonjour - الجزء 2 - إنشاء سلة التسوّق

1. مقدمة

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

تاريخ آخر تعديل: 30-10-2020

إنشاء سلة تسوّق على "الرسائل التجارية"

هذا هو الدرس العملي الثاني ضمن سلسلة تهدف إلى إنشاء رحلة مستخدم "الشراء على الإنترنت والاستلام من المتجر". في العديد من رحلات التجارة الإلكترونية، تُعدّ عربة التسوّق أساسية لنجاح تحويل المستخدمين إلى عملاء يدفعون رسومًا. تساعدك سلّة التسوّق أيضًا في فهم عملائك بشكل أفضل واقتراح منتجات أخرى قد تهمّهم. في هذا الدرس التطبيقي حول الترميز، سنركّز على إنشاء تجربة سلّة التسوّق وتفعيل التطبيق على Google App Engine.

ما الذي يجعل سلة التسوّق جيدة؟

تُعدّ عربات التسوّق أساسية لتجربة تسوّق ناجحة على الإنترنت. تبيّن أنّ "الرسائل التجارية" لا تسهّل فقط طرح الأسئلة والأجوبة حول منتج معيّن مع عميل محتمل، بل يمكنها تسهيل تجربة التسوّق بأكملها وصولاً إلى إكمال عملية الدفع ضمن المحادثة.

9d17537b980d0e62.png

بالإضافة إلى توفير سلّة تسوّق جيدة، تتيح تجربة التسوّق الجيدة للمستخدمين تصفّح السلع حسب الفئة وتسمح للنشاط التجاري باقتراح منتجات أخرى قد تهمّ المشتري. بعد إضافة المزيد من السلع إلى سلة التسوّق، يمكن للمستخدم مراجعة سلة التسوّق بأكملها، وإزالة السلع أو إضافة المزيد من السلع قبل إتمام عملية الدفع.

ما ستنشئه

في هذا القسم من سلسلة الدروس التطبيقية حول الترميز، ستوسّع نطاق عمل الوكيل الرقمي الذي أنشأته في الجزء 1 لشركة Bonjour Meal الوهمية، وذلك لكي يتمكّن المستخدمون من تصفّح قائمة المنتجات وإضافة المنتجات إلى سلّة التسوّق.

في هذا الدرس التطبيقي حول الترميز، سيتضمّن تطبيقك ما يلي:

  • عرض كتالوج أسئلة ضمن "الرسائل التجارية"
  • اقتراح عناصر قد تهمّ المستخدمين
  • مراجعة محتويات سلّة التسوّق وإنشاء ملخّص للسعر الإجمالي

ab2fb6a4ed33a129.png

ما ستتعلمه

  • كيفية نشر تطبيق ويب على App Engine في Google Cloud Platform
  • كيفية استخدام آلية تخزين ثابتة لحفظ حالة سلة التسوّق

يركّز هذا الدرس التطبيقي حول الترميز على توسيع نطاق الوكيل الرقمي من الجزء 1 من سلسلة دروس الترميز هذه.

المتطلبات

2. الإعداد

يفترض هذا الدرس التطبيقي حول الترميز أنّك أنشأت وكيلك الأول وأكملت الجزء 1 من الدرس التطبيقي حول الترميز. وبالتالي، لن نتناول أساسيات تفعيل واجهات برمجة التطبيقات الرسائل التجارية وBusiness Communications، أو إنشاء مفاتيح حساب الخدمة، أو نشر تطبيق، أو إعداد ويب هوك على Business Communications Console. بعد ذلك، سنستنسخ نموذج تطبيق للتأكّد من أنّ تطبيقك يتوافق مع ما نعمل على إنشائه، وسنفعّل واجهة برمجة التطبيقات Datastore على Google Cloud Platform لنتمكّن من الاحتفاظ بالبيانات المتعلّقة بسلّة التسوّق.

استنساخ التطبيق من GitHub

في نافذة طرفية، استنسِخ نموذج برنامج Django Echo Bot إلى دليل العمل الخاص بمشروعك باستخدام الأمر التالي:

$ git clone https://github.com/google-business-communications/bm-bonjour-meal-django-starter-code

انسخ ملف بيانات اعتماد JSON الذي تم إنشاؤه لحساب الخدمة إلى مجلد الموارد الخاص بالعينة وأعِد تسمية بيانات الاعتماد إلى "bm-agent-service-account-credentials.json".

bm-bonjour-meal-django-starter-code/bonjourmeal-codelab/step-2/resources/bm-agent-service-account-credentials.json

تفعيل Google Datastore API

في الجزء 1 من هذا الدرس العملي، فعّلت واجهة الرسائل التجارية وواجهة Business communications API وواجهة Cloud Build API.

في هذا الدرس التطبيقي حول الترميز، سنستخدم Google Datastore، لذا علينا أيضًا تفعيل واجهة برمجة التطبيقات هذه:

  1. افتح Google Datastore API في Google Cloud Console.
  2. تأكَّد من أنّك تعمل على مشروع GCP الصحيح.
  3. انقر على تفعيل.

نشر التطبيق النموذجي

في الوحدة الطرفية، انتقِل إلى دليل الخطوة 2 الخاص بالعينة.

نفِّذ الأوامر التالية في الوحدة الطرفية لنشر النموذج:

$ gcloud config set project PROJECT_ID*
$ gcloud app deploy
  • ‫PROJECT_ID هو رقم تعريف المشروع الذي استخدمته للتسجيل في واجهات برمجة التطبيقات.

دوِّن عنوان URL للتطبيق الذي تم نشره في ناتج الأمر الأخير:

Deployed service [default] to [https://PROJECT_ID.appspot.com]

يتضمّن الرمز الذي تم نشره للتو تطبيق ويب مع ويب هوك لتلقّي الرسائل من "الرسائل التجارية". يحتوي على كل ما أنجزناه في الجزء 1 من الدرس العملي. إذا لم يسبق لك إجراء ذلك، يُرجى ضبط إعدادات webhook.

سيردّ التطبيق على بعض الاستفسارات البسيطة، مثل استفسار المستخدم عن ساعات عمل مطعم Bonjour Meal. يجب اختبار ذلك على جهازك الجوّال من خلال عناوين URL الاختبارية التي يمكنك استردادها من "معلومات الوكيل" ضمن Business Communications Console. ستؤدي عناوين URL التجريبية إلى إطلاق تجربة "الرسائل التجارية" على جهازك الجوّال، ويمكنك البدء بالتفاعل مع موظّف الدعم هناك.

3- كتالوج المنتجات

نظام المستودع

في معظم الحالات، يمكنك الدمج مباشرةً مع مستودع علامة تجارية من خلال واجهة برمجة تطبيقات داخلية. في حالات أخرى، يمكنك استخراج البيانات من صفحة ويب أو إنشاء نظام خاص بك لتتبُّع المستودع. لا يهمّنا إنشاء نظام مستودع، بل سنستخدم ملفًا ثابتًا بسيطًا يحتوي على صور ومعلومات المنتجات الخاصة بالوكيل. في هذا القسم، سنستخرج المعلومات من هذا الملف الثابت، ونعرضها في المحادثة، ونسمح للمستخدم بتصفّح السلع المتاحة لإضافتها إلى سلّة التسوّق.

يظهر ملف المستودع الثابت على النحو التالي:

bonjourmeal-codelab/step-2/resources/inventory.json

{

    "food": [
        {
            "id":0,
            "name": "Ham and cheese sandwich",
            "price": "6.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/ham-and-cheese.png",
            "remaining": 8
        },
        {
            "id":1,
            "name": "Chicken veggie wrap",
            "price": "9.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/chicken-veggie-wrap.png",
            "remaining": 2
        },
        {
            "id":2,
            "name": "Assorted cheese plate",
            "price": "7.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/assorted-cheese-plate.png",
            "remaining": 6
        },
        {
            "id":3,
            "name": "Apple walnut salad",
            "price": "12.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/apple-walnut-salad.png",
            "remaining": 1
        }
    ]
}

لنبدأ بتطبيق Python لقراءة هذا الملف.

القراءة من مستودعنا

المستودع هو ملف ثابت باسم "inventory.json" موجود في الدليل ‎ ./resources. علينا إضافة بعض منطق Python إلى ملف views.py لقراءة محتوى ملف JSON ثم عرضه في المحادثة. لننشئ دالة تقرأ البيانات من ملف JSON وتعرض قائمة بالمنتجات المتاحة.

يمكن وضع تعريف هذه الدالة في أي مكان في ملف views.py.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_inventory_data():
        f = open(INVENTORY_FILE)
        inventory = json.load(f)
        return inventory
...

من المفترض أن يوفّر لنا ذلك ما نحتاج إليه لقراءة البيانات من المستودع. نحتاج الآن إلى طريقة لعرض معلومات المنتج هذه في المحادثة.

عرض كتالوج المنتجات

لتبسيط هذا الدرس التطبيقي حول الترميز، لدينا كتالوج منتجات عام لعرض جميع عناصر المستودع في محادثة "الرسائل التجارية" من خلال لوحة عرض دوّارة واحدة لبطاقات تفاعلية.

لعرض قائمة المنتجات، سننشئ ردًا مقترحًا يتضمّن النص "عرض القائمة" وpostbackData "show-product-catalog". وعندما ينقر المستخدمون على الردّ المقترح ويتلقّى تطبيق الويب بيانات postback، سنرسل لوحة عرض دوّارة للبطاقات التفاعلية. لنضِف ثابتًا جديدًا لهذا الردّ المقترَح في أعلى ملف views.py.

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_SHOW_PRODUCT_CATALOG = 'show-product-catalog'
...

من هنا، نحلّل الرسالة ونوجّهها إلى دالة جديدة تُرسِل لوحة عرض دوّارة لبطاقات تفاعلية تحتوي على كتالوج المنتجات. أولاً، وسّع الدالة route_message لاستدعاء الدالة "send_product_catalog" عند النقر على الرد المقترَح، ثم سنعرّف الدالة.

في المقتطف التالي، أضِف شرطًا إضافيًا إلى عبارة if في الدالة route_message للتحقّق مما إذا كانت normalized_message تساوي الثابت الذي حدّدناه سابقًا، CMD_SHOW_PRODUCT_CATALOG.

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATALOG:
        send_product_catalog(conversation_id)
    else:
        echo_message(message, conversation_id)
...

لنحرص على إكمال العملية وتحديد send_product_catalog. send_product_catalog calls get_menu_carousel, التي تنشئ لوحة العرض الدوّارة للبطاقات التفاعلية من ملف المستودع الذي قرأناه سابقًا.

يمكن وضع تعريفات الدوال في أي مكان في ملف views.py. يُرجى العِلم أنّ المقتطف التالي يستخدم ثابتَين جديدَين يجب إضافتهما إلى أعلى الملف.

bonjourmeal-codelab/step-2/bopis/views.py

...

CMD_ADD_ITEM = 'add-item'
CMD_SHOW_CART = 'show-cart'

...

def get_menu_carousel():
    """Creates a sample carousel rich card.

    Returns:
       A :obj: A BusinessMessagesCarouselCard object with three cards.
    """

    inventory = get_inventory_data()

    card_content = []

    for item in inventory['food']:
        card_content.append(BusinessMessagesCardContent(
            title=item['name'],
            description=item['price'],
            suggestions=[
                BusinessMessagesSuggestion(
                    reply=BusinessMessagesSuggestedReply(
                        text='Add item',
                        postbackData='{'+f'"action":"{CMD_ADD_ITEM}","item_name":"{item["id"]}"'+'}'))

                ],
            media=BusinessMessagesMedia(
                height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                contentInfo=BusinessMessagesContentInfo(
                    fileUrl=item['image_url'],
                    forceRefresh=False))))

    return BusinessMessagesCarouselCard(
        cardContents=card_content,
        cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum.MEDIUM)

def send_product_catalog(conversation_id):
    """Sends the product catalog to the conversation_id.

    Args:
        conversation_id (str): The unique id for this user and agent.
    """
    rich_card = BusinessMessagesRichCard(carouselCard=get_menu_carousel())

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
        fallback_text += (card_content.title + '\n\n' + card_content.description
                          + '\n\n' + card_content.media.contentInfo.fileUrl
                          + '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        fallback=fallback_text,
        suggestions=[
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See my cart',
                postbackData=CMD_SHOW_CART)
            ),
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See the menu',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
        ]
        )

    send_message(message_obj, conversation_id)
...

إذا فحصت عملية إنشاء عناصر لوحة العرض الدوّارة، ستلاحظ أنّنا ننشئ أيضًا مثيلاً للفئة BusinessMessagesSuggestion. يمثّل كل اقتراح اختيارًا من المستخدم لمنتج في لوحة العرض الدوّارة. عندما ينقر المستخدم على الردّ المقترَح، سترسل "الرسائل التجارية" بيانات postbackData التي تحتوي على JSON يصف السلعة والإجراء الذي يريد المستخدم اتخاذه (إضافة السلعة إلى سلة التسوق أو إزالتها منها) إلى رابط الويب. في القسم التالي، سنحلّل الرسائل التي تبدو على هذا النحو لنتمكّن من إضافة المنتج إلى سلة التسوّق.

بعد إجراء هذه التغييرات، لننشُر تطبيق الويب على Google App Engine ونجرِّب التجربة.

$ gcloud app deploy

عند تحميل مساحة العرض الحوارية على جهازك الجوّال، أرسِل الرسالة "show-product-catalog"، وسيظهر لك لوحة عرض دوّارة للمنتجات على النحو التالي.

4639da46bcc5230c.png

إذا نقرت على إضافة منتج، لن يحدث أي شيء فعليًا باستثناء أنّ الوكيل يعرض بيانات تسجيل الإحالات الناجحة من الردّ المقترَح. في القسم التالي، سنستفيد من كتالوج المنتجات ونستخدمه لإنشاء سلّة التسوّق التي سيتمّ إضافة المنتج إليها.

يمكن توسيع نطاق كتالوج المنتجات الذي أنشأته للتو بطرق متنوعة. قد تتوفّر لديك خيارات مختلفة في قائمة المشروبات أو خيارات نباتية. يُعد استخدام لوحات العرض الدوّارة أو شرائح الاقتراحات طريقة رائعة للسماح للمستخدمين بالبحث بالتفصيل في خيارات القائمة للوصول إلى مجموعة من المنتجات التي يبحثون عنها. كإضافة إلى هذا الدرس التطبيقي حول الترميز، حاوِل توسيع نظام كتالوج المنتجات ليتمكّن المستخدم من عرض المشروبات بشكل منفصل عن الطعام في القائمة، أو حتى تحديد خيارات نباتية.

4. سلّة التسوّق

في هذا القسم من الدرس العملي، سننشئ وظيفة سلة التسوّق استنادًا إلى القسم السابق الذي يتيح لنا تصفّح المنتجات المتاحة.

تتيح تجربة سلة التسوّق الرئيسية للمستخدمين، من بين العديد من الميزات، إضافة سلع إلى السلة وإزالتها منها وتتبُّع عدد كل سلعة في السلة ومراجعة السلع فيها.

يعني تتبُّع حالة سلة التسوّق أنّه علينا الاحتفاظ بالبيانات على تطبيق الويب. لتسهيل عملية التجربة والنشر، سنستخدم Google Datastore في Google Cloud Platform لتخزين البيانات بشكل دائم. يظلّ معرّف المحادثة ثابتًا بين المستخدم والنشاط التجاري، لذا يمكننا استخدامه لربط المستخدمين بسلع سلة التسوّق.

لنبدأ بالاتصال بخدمة Google Datastore وتخزين معرّف المحادثة عند ظهوره.

الربط بخدمة Datastore

سنتواصل مع Google Datastore كلّما تمّ تنفيذ أيّ تفاعل مع سلّة التسوّق، مثلاً عندما يضيف مستخدم سلعةً أو يحذفها. يمكنك الاطّلاع على مزيد من المعلومات حول استخدام مكتبة البرامج هذه للتفاعل مع Google Datastore في المستندات الرسمية.

يحدّد المقتطف التالي دالة لتعديل سلّة التسوّق. تتلقّى الدالة الإدخال التالي: conversation_id وmessage. يحتوي message على JSON يصف الإجراء الذي يريد المستخدم اتّخاذه، وهو مضمّن في لوحة العرض الدوّارة التي تعرض كتالوج المنتجات. تنشئ الدالة عميل Google Datastore وتجلب على الفور كيان ShoppingCart، حيث يكون المفتاح هو معرّف المحادثة.

انسخ الدالة التالية إلى ملف views.py. وسنواصل التوسّع في هذا الموضوع في القسم التالي.

bonjourmeal-codelab/step-2/bopis/views.py

from google.oauth2 import service_account
from google.cloud import datastore

def update_shopping_cart(conversation_id, message):
        credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_LOCATION)

        client = datastore.Client(credentials=credentials)
        key = client.key('ShoppingCart', conversation_id)
        entity = datastore.Entity(key=key)
        result = client.get(key)
        
        # TODO: Add logic to add and remove items from cart
        
        entity.update(result)
        client.put(entity)

لنوسّع نطاق هذه الدالة لإضافة سلعة إلى سلة التسوّق.

إضافة سلع إلى سلة التسوّق

عندما ينقر المستخدم على الإجراء المقترَح إضافة عنصر من لوحة عرض المنتجات الدوّارة، يحتوي postbackData على ملف JSON يصف الإجراء الذي يريد المستخدم اتّخاذه. يحتوي قاموس JSON على مفتاحَين، هما "action" و "item_name"، ويتم إرسال قاموس JSON هذا إلى خطاف الويب. الحقل "item_name" هو المعرّف الفريد المرتبط بالسلعة في ملف inventory.json.

بعد أن نحصل على أمر سلة التسوّق وعنصر سلة التسوّق الذي تم تحليله من الرسالة، يمكننا بعد ذلك كتابة عبارات شرطية لإضافة العنصر. بعض الحالات المتقدّمة التي يجب التفكير فيها هنا هي ما إذا كان Datastore لم يسبق له أن رأى معرّف المحادثة أو ما إذا كانت سلّة التسوّق تتلقّى هذه السلعة للمرة الأولى. في ما يلي توسيع لوظيفة update_shopping_cart المحدّدة أعلاه. يضيف هذا التغيير عنصرًا إلى سلّة التسوّق التي يتم الاحتفاظ بها في Google Datastore.

المقتطف التالي هو امتداد للدالة السابقة التي تمت إضافتها إلى ملف views.py. يمكنك إضافة الفرق أو نسخ المقتطف واستبدال النسخة الحالية من الدالة update_shopping_cart.

bonjourmeal-codelab/step-2bopis/views.py

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']

    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        entity.update(result)
    client.put(entity)

سيتم توسيع نطاق هذه الدالة لاحقًا لتشمل الحالة التي تحتوي فيها cart_cmd على السلسلة "del-item" المحدّدة في CMD_DEL_ITEM.

ربط كل ذلك معًا

تأكَّد من إضافة السباكة في الدالة route_message حتى يتم استدعاء الدالة update_shopping_cart إذا تلقّيت رسالة لإضافة سلعة إلى سلة التسوّق. عليك أيضًا تحديد ثابت لإضافة عناصر باستخدام الاصطلاح الذي نستخدمه في جميع أنحاء الدرس التطبيقي حول الترميز.

bonjourmeal-codelab/step-2bopis/views.py

...

CMD_DEL_ITEM = 'del-item'

...

def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
       update_shopping_cart(conversation_id, message)
    else:
        echo_message(message, conversation_id)

...

في الوقت الحالي، يمكننا إضافة سلع إلى سلة التسوّق. إذا نشرت تغييراتك على Google App Engine، من المفترض أن تتمكّن من رؤية التغييرات في سلّة التسوّق في لوحة بيانات Google Datastore ضمن وحدة تحكّم Google Cloud Platform. في لقطة الشاشة أدناه لوحدة تحكّم Google Datastore، هناك عنصر واحد يحمل اسم معرّف المحادثة متبوعًا ببعض العلاقات مع سلع المستودع وكمية تلك السلع في سلة التسوّق.

619dc18a8136ea69.png

في القسم التالي، سننشئ طريقة لإدراج العناصر في سلّة التسوّق. يجب أن تعرض آلية مراجعة سلة التسوّق جميع السلع الموجودة في السلة وكميتها وخيار إزالة سلعة من السلة.

مراجعة السلع في سلة التسوّق

إنّ إدراج السلع في سلة التسوّق هو الطريقة الوحيدة التي يمكننا من خلالها فهم حالة سلة التسوّق ومعرفة السلع التي يمكننا إزالتها.

لنبدأ بإرسال رسالة ودية مثل "إليك سلة التسوّق:"، تليها رسالة أخرى تحتوي على لوحة دوّارة لبطاقة تفاعلية مع ردود مقترَحة ذات صلة مثل "إزالة منتج واحد" أو "إضافة منتج واحد". يجب أن يعرض دوّار البطاقات التفاعلية أيضًا عدد السلع المحفوظة في سلّة التسوّق.

قبل أن نبدأ بكتابة الدالة، يجب أن نضع في اعتبارنا أنّه إذا كان هناك نوع واحد فقط من العناصر في سلّة التسوّق، لا يمكننا عرضها كلوحة دوّارة. يجب أن تحتوي منصّات العرض الدوّارة للبطاقات التفاعلية على بطاقتَين على الأقل. في المقابل، إذا لم تكن هناك أي سلع في سلة التسوق، نريد عرض رسالة بسيطة تفيد بأنّ سلة التسوق فارغة.

مع أخذ ذلك في الاعتبار، لنحدّد دالة باسم send_shopping_cart. ترتبط هذه الدالة بخدمة Google Datastore وتطلب كيان ShoppingCart استنادًا إلى معرّف المحادثة. بعد الحصول على هذه المعلومات، سنستدعي الدالة get_inventory_data ونستخدم لوحة عرض دوّارة لبطاقة تفاعلية من أجل عرض حالة سلّة التسوّق. سنحتاج أيضًا إلى الحصول على معرّف منتج حسب الاسم، ويمكننا تعريف دالة للبحث في Google Datastore لتحديد هذه القيمة. أثناء إنشاء لوحة العرض الدوّارة، يمكننا ربط الردود المقترَحة بحذف العناصر أو إضافتها حسب معرّف المنتج. تنفِّذ المقتطفة البرمجية أدناه كل هذه العمليات. انسخ الرمز في أي مكان في ملف views.py.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_id_by_product_name(product_name):
  inventory = get_inventory_data()
  for item in inventory['food']:
    if item['name'] == product_name:
      return int(item['id'])
  return False


def send_shopping_cart(conversation_id):
  credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)

  # Retrieve the inventory data
  inventory = get_inventory_data()

  # Pull the data from Google Datastore
  client = datastore.Client(credentials=credentials)
  key = client.key('ShoppingCart', conversation_id)
  result = client.get(key)

  shopping_cart_suggestions = [
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See total price', postbackData='show-cart-price')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='Empty the cart', postbackData='empty-cart')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See the menu', postbackData=CMD_SHOW_PRODUCT_CATALOG)),
  ]

  if result is None or len(result.items()) == 0:
    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text='There are no items in your shopping cart.',
        suggestions=shopping_cart_suggestions)

    send_message(message_obj, conversation_id)
  elif len(result.items()) == 1:

    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      fallback_text = ('You have one type of item in the shopping cart')

      rich_card = BusinessMessagesRichCard(
          standaloneCard=BusinessMessagesStandaloneCard(
              cardContent=BusinessMessagesCardContent(
                  title=product_name,
                  description=f'{quantity} in cart.',
                  suggestions=[
                      BusinessMessagesSuggestion(
                          reply=BusinessMessagesSuggestedReply(
                              text='Remove one',
                              postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
                  ],
                  media=BusinessMessagesMedia(
                      height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                      contentInfo=BusinessMessagesContentInfo(
                          fileUrl=inventory['food'][product_id]
                          ['image_url'],
                          forceRefresh=False)))))

      message_obj = BusinessMessagesMessage(
          messageId=str(uuid.uuid4().int),
          representative=BOT_REPRESENTATIVE,
          richCard=rich_card,
          suggestions=shopping_cart_suggestions,
          fallback=fallback_text)

      send_message(message_obj, conversation_id)
  else:
    cart_carousel_items = []

    # Iterate through the cart and generate a carousel of items
    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      cart_carousel_items.append(
          BusinessMessagesCardContent(
              title=product_name,
              description=f'{quantity} in cart.',
              suggestions=[
                  BusinessMessagesSuggestion(
                      reply=BusinessMessagesSuggestedReply(
                          text='Remove one',
                          postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
              ],
              media=BusinessMessagesMedia(
                  height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                  contentInfo=BusinessMessagesContentInfo(
                      fileUrl=inventory['food'][product_id]
                      ['image_url'],
                      forceRefresh=False))))

    rich_card = BusinessMessagesRichCard(
        carouselCard=BusinessMessagesCarouselCard(
            cardContents=cart_carousel_items,
            cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum
            .MEDIUM))

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
      fallback_text += (
          card_content.title + '\n\n' + card_content.description + '\n\n' +
          card_content.media.contentInfo.fileUrl +
          '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        suggestions=shopping_cart_suggestions,
        fallback=fallback_text,
    )

    send_message(message_obj, conversation_id)

...

تأكَّد من أنّك حدّدت CMD_SHOW_CART في أعلى ملف views.py واستدعِ send_shopping_cart إذا أرسل المستخدم رسالة تتضمّن show-cart.

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    else:
        echo_message(message, conversation_id)
...

34801776a97056ac.png

استنادًا إلى المنطق الذي قدّمناه في الدالة send_shopping_cart، عندما تكتب "عرض-السلة"، ستتلقّى إما رسالة تفيد بأنّ السلة فارغة، أو بطاقة تفاعلية تعرض المنتج الوحيد في السلة، أو لوحة عرض دوّارة للبطاقات تعرض منتجات متعددة. بالإضافة إلى ذلك، لدينا ثلاثة ردود مقترَحة: "الاطّلاع على السعر الإجمالي" و"إفراغ سلة التسوّق" و "الاطّلاع على قائمة الطعام".

جرِّب نشر تغييرات الرمز البرمجي أعلاه لاختبار ما إذا كانت سلة التسوّق تتتبّع السلع التي تضيفها وما إذا كان بإمكانك مراجعة السلة من مساحة المحادثات كما هو موضّح في لقطات الشاشة أعلاه. يمكنك نشر التغييرات باستخدام هذا الأمر الذي يتم تنفيذه من دليل step-2 الذي تضيف فيه تغييراتك.

$ gcloud app deploy

سننشئ ميزة "الاطّلاع على السعر الإجمالي" في القسم التالي بعد إنشاء وظيفة إزالة منتج من سلّة التسوّق. ستتصرّف الدالة get_cart_price بشكل مشابه لميزة "عرض سلّة التسوّق" من حيث أنّها ستقارن البيانات في مخزن البيانات مع ملف JSON لإنتاج السعر الإجمالي لسلّة التسوّق. سيكون هذا مفيدًا في الجزء التالي من الدرس العملي الذي ندمج فيه نظام الدفع.

إزالة سلع من سلة التسوّق

أخيرًا، يمكننا إكمال سلوك سلة التسوّق من خلال توفير وظيفة لإزالة السلة. استبدِل الدالة update_shopping_cart الحالية بالمقتطف التالي.

bonjourmeal-codelab/step-2/‎ bopis/views.py

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']


    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })
        elif cart_cmd == CMD_DEL_ITEM:
            # The user is trying to delete an item from an empty cart. Pass and skip
            pass

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        elif cart_cmd == CMD_DEL_ITEM:
            if result.get(item_name) is None:
                # The user is trying to remove an item that's no in the shopping cart. Pass and skip
                pass
            elif result[item_name] - 1 > 0:
                result[item_name] = result[item_name] - 1
            else:
                del result[item_name]

        entity.update(result)
    client.put(entity)

إرسال رسالة تأكيد

عندما يضيف المستخدم منتجًا إلى سلة التسوّق، عليك إرسال رسالة تأكيد لإعلامه بأنّك تلقّيت طلبه وعالجته. لا يساعد ذلك في تحديد التوقعات فحسب، بل يساهم أيضًا في استمرار المحادثة.

لنوسّع وظيفة update_shopping_cart لتتمكّن من إرسال رسالة إلى رقم تعريف المحادثة تفيد بأنّه تمّت إضافة السلعة أو إزالتها، وتقديم اقتراحات لمراجعة سلّة التسوّق أو عرض القائمة مرة أخرى.

bonjourmeal-codelab/step-2/bopis/views.py

def update_shopping_cart(conversation_id, message):

     # No changes to the function, except appending the following logic
     ...
   
    if cart_cmd == CMD_ADD_ITEM:
        message = 'Great! You\'ve added an item to the cart.'
    else:
        message = 'You\'ve removed an item from the cart.'

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text=message,
        suggestions=[
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='Review shopping cart',
                postbackData=CMD_SHOW_CART)
            ),
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See menu again',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
            ])
    send_message(message_obj, conversation_id)

905a1f3d89893ba0.png

هذا كل ما عليك فعله. تجربة سلّة تسوّق كاملة الميزات تتيح للمستخدم إضافة سلع وإزالتها ومراجعتها في السلّة

في هذه المرحلة، إذا أردت الاطّلاع على وظيفة سلّة التسوّق في محادثة "الرسائل التجارية"، عليك نشر التطبيق للتفاعل مع الوكيل. يمكنك إجراء ذلك من خلال تنفيذ هذا الأمر في دليل الخطوة 2.

$ gcloud app deploy

5- الاستعداد لتلقّي الدفعات

استعدادًا لعملية الدمج مع الجهة المسؤولة عن معالجة المعاملات في الجزء التالي من السلسلة، نحتاج إلى طريقة للحصول على سعر سلّة التسوّق. لننشئ دالة تسترد السعر من خلال الرجوع إلى بيانات سلّة التسوّق في Google Datastore، واسترداد سعر كل سلعة من المستودع، وضرب السعر في كمية كل سلعة في السلّة.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_cart_price(conversation_id):
    # Pull the data from Google Datastore
    credentials = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_LOCATION)
    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    # Retrieve the inventory data
    inventory = get_inventory_data()
   
    # Start off with a total of 0 before adding up the total
    total_price = 0

    if len(result.items()) != 0:
      for product_name, quantity in result.items():
        total_price = total_price + float(
            inventory['food'][get_id_by_product_name(product_name)]['price']) * int(quantity)

    return total_price

...

وأخيرًا، يمكننا استخدام هذه الدالة وإرسال رسالة إلى المستخدم.

bonjourmeal-codelab/step-2/bopis/views.py

...

def send_shopping_cart_total_price(conversation_id):
    cart_price = get_cart_price(conversation_id)

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        suggestions=[],
        text=f'Your cart\'s total price is ${cart_price}.')

    send_message(message_obj, conversation_id)
...

لربط كل ذلك معًا، لنعدّل الدالة route_message والثابت لتفعيل المنطق أعلاه.

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_GET_CART_PRICE = 'show-cart-price'
...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    elif normalized_message == CMD_GET_CART_PRICE:
        send_shopping_cart_total_price(conversation_id)
    else:
        echo_message(message, conversation_id)
...

في ما يلي بعض لقطات الشاشة لعرض ما تحقّقه المنطق أعلاه:

8feacf94ed0ac6c4.png

عندما نكون مستعدين للتكامل مع الجهة المسؤولة عن معالجة المعاملات في الجزء التالي من الدرس التطبيقي حول الترميز، سنستدعي الدالة get_cart_price لتمرير البيانات إلى الجهة المسؤولة عن معالجة المعاملات وبدء مسار الدفع.

يمكنك تجربة وظيفة سلّة التسوّق هذه في محادثة "الرسائل التجارية" من خلال نشر التطبيق والتفاعل مع الوكيل.

$ gcloud app deploy

6. تهانينا

تهانينا، لقد أنشأت تجربة سلّة تسوّق بنجاح ضمن ميزة "الرسائل التجارية".

من الميزات التي لم نتناولها في هذا الدرس العملي هي ميزة إفراغ سلة التسوّق بأكملها. إذا أردت، يمكنك محاولة توسيع نطاق التطبيق لتنفيذ ميزة "إفراغ سلة التسوّق". يتوفّر الحلّ في الخطوة 3 من رمز المصدر الذي نسخته.

في قسم مستقبلي، سنعمل على الدمج مع خدمة خارجية لمعالجة الدفعات من أجل السماح للمستخدمين بإكمال معاملة دفع مع علامتك التجارية.

ما الذي يجعل سلة التسوّق جيدة؟

لا تختلف تجربة استخدام سلّة التسوّق في محادثة عن تجربة استخدام تطبيق على الجهاز الجوّال أو في متجر عادي. تُعدّ القدرة على إضافة سلع وإزالتها واحتساب سعر سلة التسوّق بعض الميزات التي استكشفناها في هذا الدرس التطبيقي حول الترميز. تختلف سلة التسوّق الافتراضية عن سلة التسوّق الحقيقية في إمكانية الاطّلاع على سعر جميع السلع في السلة في أي لحظة، وذلك عند إضافة السلع أو إزالتها. ستساعدك هذه الأنواع من الميزات القيّمة في إبراز تجربة التجارة الحوارية التي تقدّمها.

ما هي الخطوات التالية؟

عندما تكون مستعدًا، يمكنك الاطّلاع على بعض المواضيع التالية للتعرّف على التفاعلات الأكثر تعقيدًا التي يمكنك تحقيقها في "الرسائل التجارية":

المستندات المرجعية