diff --git a/CHANGELOG b/CHANGELOG index a41b77776f..7c3b6f2889 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Support webhook for orders * Remove API backoff time * Support edit orders from Shopify * Define the metafields managed by Tryton diff --git a/__init__.py b/__init__.py index 5257d86608..374f19ad1b 100644 --- a/__init__.py +++ b/__init__.py @@ -3,9 +3,10 @@ from trytond.pool import Pool -from . import account, ir, party, product, sale, shopify_retry, stock, web +from . import ( + account, ir, party, product, routes, sale, shopify_retry, stock, web) -__all__ = ['register'] +__all__ = ['register', 'routes'] shopify_retry.patch() diff --git a/doc/usage.rst b/doc/usage.rst index 646acc35d8..68ef35a326 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -34,3 +34,6 @@ the "Password" with the copied one. Different scheduled tasks are responsible for uploading products and inventories and fetching and updating orders as `Sales `. + +You can also register a `webhook `_ for each +order event to get updates when they happen. diff --git a/ir.xml b/ir.xml index e3e1908a6b..880e6331a7 100644 --- a/ir.xml +++ b/ir.xml @@ -17,14 +17,14 @@ this repository contains the full copyright notices and license terms. --> web.shop|shopify_fetch_order - - minutes + + days web.shop|shopify_update_order - - minutes + + days diff --git a/routes.py b/routes.py new file mode 100644 index 0000000000..fdb092d560 --- /dev/null +++ b/routes.py @@ -0,0 +1,63 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +import base64 +import hashlib +import hmac +import logging + +try: + from http import HTTPStatus +except ImportError: + from http import client as HTTPStatus + +from werkzeug.exceptions import abort +from werkzeug.wrappers import Response + +from trytond.protocols.wrappers import with_pool, with_transaction +from trytond.wsgi import app + +logger = logging.getLogger(__name__) + + +def verify_webhook(data, hmac_header, secret): + digest = hmac.new(secret, data, hashlib.sha256).digest() + computed_hmac = base64.b64encode(digest) + return hmac.compare_digest(computed_hmac, hmac_header.encode('utf-8')) + + +@app.route( + '//web_shop_shopify/webhook//order', methods={'POST'}) +@with_pool +@with_transaction(context={'_skip_warnings': True}) +def order(request, pool, shop): + Sale = pool.get('sale.sale') + Shop = pool.get('web.shop') + shop = Shop.get(shop) + data = request.get_data() + verified = verify_webhook( + data, request.headers.get('X-Shopify-Hmac-SHA256'), + shop.shopify_webhook_shared_secret.encode('utf-8')) + if not verified: + abort(HTTPStatus.UNAUTHORIZED) + + topic = request.headers.get('X-Shopify-Topic') + order = request.get_json() + if topic == 'orders/create': + if not Sale.search([ + ('web_shop', '=', shop.id), + ('shopify_identifier', '=', order['id']), + ], order=[], limit=1): + Shop.__queue__.shopify_fetch_order([shop]) + elif topic in {'orders/updated', 'orders/payment'}: + sales = Sale.search([ + ('web_shop', '=', shop.id), + ('shopify_identifier', '=', order['id']), + ], order=[], limit=1) + if not sales: + Shop.__queue__.shopify_fetch_order([shop]) + else: + sale, = sales + Shop.__queue__.shopify_update_sale([sale], [order]) + else: + logger.info("Unsupported topic '%s'", topic) + return Response(status=HTTPStatus.NO_CONTENT) diff --git a/view/shop_form.xml b/view/shop_form.xml index 436f92f287..ac55eaa9e4 100644 --- a/view/shop_form.xml +++ b/view/shop_form.xml @@ -16,6 +16,11 @@ this repository contains the full copyright notices and license terms. -->