+ CDEK webhook for instant status updates
This commit is contained in:
parent
46238cab4b
commit
c47864106e
|
|
@ -1,4 +1,5 @@
|
||||||
APP_HOME="/var/www/poizonstore-stage"
|
APP_HOME="/var/www/poizonstore-stage"
|
||||||
|
SITE_URL="https://crm-poizonstore.ru"
|
||||||
|
|
||||||
# === Keys ===
|
# === Keys ===
|
||||||
# Django
|
# Django
|
||||||
|
|
@ -12,6 +13,7 @@ TG_BOT_TOKEN=""
|
||||||
# External API settings
|
# External API settings
|
||||||
CDEK_CLIENT_ID=""
|
CDEK_CLIENT_ID=""
|
||||||
CDEK_CLIENT_SECRET=""
|
CDEK_CLIENT_SECRET=""
|
||||||
|
CDEK_WEBHOOK_URL_SALT=""
|
||||||
POIZON_TOKEN=""
|
POIZON_TOKEN=""
|
||||||
CURRENCY_GETGEOIP_API_KEY=""
|
CURRENCY_GETGEOIP_API_KEY=""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
|
from poizonstore.utils import deep_get
|
||||||
|
from store.models import Checklist
|
||||||
from store.utils import is_migration_running
|
from store.utils import is_migration_running
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
|
||||||
|
|
@ -79,12 +81,23 @@ class CDEKStatus:
|
||||||
POSTOMAT_RECEIVED = "POSTOMAT_RECEIVED"
|
POSTOMAT_RECEIVED = "POSTOMAT_RECEIVED"
|
||||||
|
|
||||||
|
|
||||||
|
class CDEKWebhookTypes:
|
||||||
|
ORDER_STATUS = "ORDER_STATUS"
|
||||||
|
|
||||||
|
|
||||||
|
CDEK_STATUS_TO_ORDER_STATUS = {
|
||||||
|
CDEKStatus.DELIVERED: Checklist.Status.COMPLETED,
|
||||||
|
CDEKStatus.READY_FOR_SHIPMENT_IN_SENDER_CITY: Checklist.Status.CDEK,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CDEKClient:
|
class CDEKClient:
|
||||||
AUTH_ENDPOINT = 'oauth/token'
|
AUTH_ENDPOINT = 'oauth/token'
|
||||||
ORDER_INFO_ENDPOINT = 'orders'
|
ORDER_INFO_ENDPOINT = 'orders'
|
||||||
CALCULATOR_TARIFF_ENDPOINT = 'calculator/tariff'
|
CALCULATOR_TARIFF_ENDPOINT = 'calculator/tariff'
|
||||||
CALCULATOR_TARIFF_LIST_ENDPOINT = 'calculator/tarifflist'
|
CALCULATOR_TARIFF_LIST_ENDPOINT = 'calculator/tarifflist'
|
||||||
BARCODE_ENDPOINT = 'print/barcodes'
|
BARCODE_ENDPOINT = 'print/barcodes'
|
||||||
|
WEBHOOK_ENDPOINT = 'webhooks'
|
||||||
|
|
||||||
MAX_RETRIES = 2
|
MAX_RETRIES = 2
|
||||||
|
|
||||||
|
|
@ -204,8 +217,26 @@ class CDEKClient:
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def setup_webhooks(self):
|
||||||
|
if not settings.SITE_URL:
|
||||||
|
return
|
||||||
|
|
||||||
|
request_data = {
|
||||||
|
"type": CDEKWebhookTypes.ORDER_STATUS,
|
||||||
|
"url": f"{settings.SITE_URL}/cdek/webhook/{settings.CDEK_WEBHOOK_URL_SALT}/"
|
||||||
|
}
|
||||||
|
return self.request('POST', self.WEBHOOK_ENDPOINT, json=request_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def process_orderstatus_webhook(data) -> tuple:
|
||||||
|
""" Unpack CDEK request to data. Info: https://api-docs.cdek.ru/29924139.html """
|
||||||
|
cdek_number = deep_get(data, "attributes", "cdek_number")
|
||||||
|
cdek_status = deep_get(data, "attributes", "code")
|
||||||
|
return cdek_number, cdek_status
|
||||||
|
|
||||||
|
|
||||||
client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET)
|
client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET)
|
||||||
|
|
||||||
if not is_migration_running():
|
if not is_migration_running():
|
||||||
client.authorize()
|
client.authorize()
|
||||||
|
client.setup_webhooks()
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,12 @@ def get_secret(setting):
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = get_secret("SECRET_KEY")
|
SECRET_KEY = get_secret("SECRET_KEY")
|
||||||
|
SITE_URL = get_secret("SITE_URL")
|
||||||
|
|
||||||
# External API settings
|
# External API settings
|
||||||
CDEK_CLIENT_ID = get_secret("CDEK_CLIENT_ID")
|
CDEK_CLIENT_ID = get_secret("CDEK_CLIENT_ID")
|
||||||
CDEK_CLIENT_SECRET = get_secret("CDEK_CLIENT_SECRET")
|
CDEK_CLIENT_SECRET = get_secret("CDEK_CLIENT_SECRET")
|
||||||
|
CDEK_WEBHOOK_URL_SALT = get_secret("CDEK_WEBHOOK_URL_SALT")
|
||||||
|
|
||||||
POIZON_TOKEN = get_secret("POIZON_TOKEN")
|
POIZON_TOKEN = get_secret("POIZON_TOKEN")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
from rest_framework.fields import DecimalField
|
from rest_framework.fields import DecimalField
|
||||||
|
|
||||||
|
|
||||||
class PriceField(DecimalField):
|
class PriceField(DecimalField):
|
||||||
def __init__(self, *args, max_digits=10, decimal_places=2, min_value=0, **kwargs):
|
def __init__(self, *args, max_digits=10, decimal_places=2, min_value=0, **kwargs):
|
||||||
super().__init__(*args, max_digits=max_digits, decimal_places=decimal_places, min_value=min_value, **kwargs)
|
super().__init__(*args, max_digits=max_digits, decimal_places=decimal_places, min_value=min_value, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def deep_get(dictionary, *keys, default=None):
|
||||||
|
"""Get value from a nested dictionary (JSON)"""
|
||||||
|
return reduce(lambda d, key: d.get(key, None) if isinstance(d, dict) else default, keys, dictionary)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ from .models import Checklist, GlobalSettings
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def check_cdek_status(order_id):
|
def check_cdek_status(order_id):
|
||||||
|
"""Manually check CDEK status of order"""
|
||||||
|
|
||||||
obj = Checklist.objects.filter(id=order_id).first()
|
obj = Checklist.objects.filter(id=order_id).first()
|
||||||
if obj is None or obj.cdek_tracking is None or obj.status == Checklist.Status.COMPLETED:
|
if obj is None or obj.cdek_tracking is None or obj.status == Checklist.Status.COMPLETED:
|
||||||
return
|
return
|
||||||
|
|
@ -20,6 +22,7 @@ def check_cdek_status(order_id):
|
||||||
|
|
||||||
old_status = obj.status
|
old_status = obj.status
|
||||||
new_status = obj.status
|
new_status = obj.status
|
||||||
|
|
||||||
if CDEKStatus.DELIVERED in statuses:
|
if CDEKStatus.DELIVERED in statuses:
|
||||||
new_status = Checklist.Status.COMPLETED
|
new_status = Checklist.Status.COMPLETED
|
||||||
elif CDEKStatus.READY_FOR_SHIPMENT_IN_SENDER_CITY in statuses:
|
elif CDEKStatus.READY_FOR_SHIPMENT_IN_SENDER_CITY in statuses:
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from external_api.cdek import CDEKClient
|
from external_api.cdek import CDEKClient, CDEKWebhookTypes, CDEK_STATUS_TO_ORDER_STATUS
|
||||||
from external_api.poizon import PoizonClient
|
from external_api.poizon import PoizonClient
|
||||||
from utils.exceptions import CRMException
|
from utils.exceptions import CRMException
|
||||||
from store.filters import GiftFilter, ChecklistFilter
|
from store.filters import GiftFilter, ChecklistFilter
|
||||||
|
|
@ -350,6 +350,35 @@ class CDEKAPI(viewsets.GenericViewSet):
|
||||||
r = self.client.calculate_tarifflist(data)
|
r = self.client.calculate_tarifflist(data)
|
||||||
return prepare_external_response(r)
|
return prepare_external_response(r)
|
||||||
|
|
||||||
|
@action(url_path=f'webhook/{settings.CDEK_WEBHOOK_URL_SALT}', detail=False, methods=['post'])
|
||||||
|
def webhook(self, request, *args, **kwargs):
|
||||||
|
data = request.data
|
||||||
|
response = Response()
|
||||||
|
|
||||||
|
match data.get("type"):
|
||||||
|
case CDEKWebhookTypes.ORDER_STATUS:
|
||||||
|
cdek_number, cdek_status = self.client.process_orderstatus_webhook(data)
|
||||||
|
if cdek_number is None or cdek_status is None:
|
||||||
|
return response
|
||||||
|
|
||||||
|
order = Checklist.objects.filter(cdek_tracking=cdek_number).first()
|
||||||
|
if order is None:
|
||||||
|
return response
|
||||||
|
|
||||||
|
# New status or old one
|
||||||
|
new_order_status = CDEK_STATUS_TO_ORDER_STATUS.get(cdek_status, order.status)
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
if order.status != new_order_status:
|
||||||
|
print(f'Order [{order.id}] status: {order.status} -> {new_order_status}')
|
||||||
|
order.status = new_order_status
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
# TODO: review permissions
|
# TODO: review permissions
|
||||||
class PoizonAPI(viewsets.GenericViewSet):
|
class PoizonAPI(viewsets.GenericViewSet):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user