Support webhook for orders
issue11086 review377741007
This commit is contained in:
1 parent
6a50df5764
commit
4594c27fda
7 files changed
+105
-6
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Support webhook for orders
|
||||
* Remove API backoff time
|
||||
* Support edit orders from Shopify
|
||||
* Define the metafields managed by Tryton
|
||||
|
||||
+3
-2
@@ -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()
|
||||
|
||||
|
||||
@@ -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 <sale:model-sale.sale>`.
|
||||
|
||||
You can also register a `webhook <https://shopify.dev/apps/webhooks>`_ for each
|
||||
order event to get updates when they happen.
|
||||
@@ -17,14 +17,14 @@ this repository contains the full copyright notices and license terms. -->
|
||||
|
||||
<record model="ir.cron" id="cron_fetch_order">
|
||||
<field name="method">web.shop|shopify_fetch_order</field>
|
||||
<field name="interval_number" eval="15"/>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="interval_number" eval="1"/>
|
||||
<field name="interval_type">days</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="cron_update_order">
|
||||
<field name="method">web.shop|shopify_update_order</field>
|
||||
<field name="interval_number" eval="30"/>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="interval_number" eval="1"/>
|
||||
<field name="interval_type">days</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -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(
|
||||
'/<database_name>/web_shop_shopify/webhook/<shop>/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)
|
||||
@@ -16,6 +16,11 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<label name="shopify_version"/>
|
||||
<field name="shopify_version"/>
|
||||
|
||||
<label name="shopify_webhook_shared_secret"/>
|
||||
<field name="shopify_webhook_shared_secret"/>
|
||||
<label name="shopify_webhook_endpoint_order"/>
|
||||
<field name="shopify_webhook_endpoint_order"/>
|
||||
|
||||
<field name="shopify_payment_journals" colspan="6"/>
|
||||
</page>
|
||||
</xpath>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# 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 datetime as dt
|
||||
import urllib.parse
|
||||
from decimal import Decimal
|
||||
|
||||
import pyactiveresource
|
||||
@@ -16,6 +17,7 @@ from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval
|
||||
from trytond.tools import grouped_slice
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.url import http_host
|
||||
|
||||
from .common import IdentifierMixin, IdentifiersMixin
|
||||
from .exceptions import ShopifyError
|
||||
@@ -37,6 +39,17 @@ class Shop(metaclass=PoolMeta):
|
||||
'get_shopify_versions', "Version", states=_states, depends=_depends)
|
||||
shopify_password = fields.Char(
|
||||
"Password", states=_states, depends=_depends)
|
||||
shopify_webhook_shared_secret = fields.Char(
|
||||
"Webhook Shared Secret",
|
||||
states={
|
||||
'invisible': _states['invisible'],
|
||||
},
|
||||
depends=_depends)
|
||||
shopify_webhook_endpoint_order = fields.Function(
|
||||
fields.Char(
|
||||
"Webhook Order Endpoint",
|
||||
help="The URL to be called by Shopify for Order events."),
|
||||
'on_change_with_shopify_webhook_endpoint_order')
|
||||
shopify_warehouses = fields.One2Many(
|
||||
'web.shop-stock.location', 'shop', "Warehouses",
|
||||
states=_states, depends=_depends)
|
||||
@@ -63,6 +76,19 @@ class Shop(metaclass=PoolMeta):
|
||||
return [(None, "")] + sorted(
|
||||
((v, v) for v in ApiVersion.versions), reverse=True)
|
||||
|
||||
@fields.depends('name')
|
||||
def on_change_with_shopify_webhook_endpoint_order(self, name=None):
|
||||
if not self.name:
|
||||
return
|
||||
url_part = {
|
||||
'database_name': Transaction().database.name,
|
||||
'shop': self.name,
|
||||
}
|
||||
return http_host() + (
|
||||
urllib.parse.quote(
|
||||
'/%(database_name)s/web_shop_shopify/webhook/%(shop)s/order' %
|
||||
url_part))
|
||||
|
||||
@classmethod
|
||||
def view_attributes(cls):
|
||||
return super().view_attributes() + [
|
||||
|
||||
Reference in new issue
Block a user