+ CDEK webhook for instant status updates

This commit is contained in:
Phil Zhitnikov 2024-05-24 02:08:03 +04:00
parent 46238cab4b
commit c47864106e
6 changed files with 75 additions and 1 deletions

View File

@ -1,4 +1,5 @@
APP_HOME="/var/www/poizonstore-stage"
SITE_URL="https://crm-poizonstore.ru"
# === Keys ===
# Django
@ -12,6 +13,7 @@ TG_BOT_TOKEN=""
# External API settings
CDEK_CLIENT_ID=""
CDEK_CLIENT_SECRET=""
CDEK_WEBHOOK_URL_SALT=""
POIZON_TOKEN=""
CURRENCY_GETGEOIP_API_KEY=""

View File

@ -9,6 +9,8 @@ import requests
from django.conf import settings
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
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
@ -79,12 +81,23 @@ class CDEKStatus:
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:
AUTH_ENDPOINT = 'oauth/token'
ORDER_INFO_ENDPOINT = 'orders'
CALCULATOR_TARIFF_ENDPOINT = 'calculator/tariff'
CALCULATOR_TARIFF_LIST_ENDPOINT = 'calculator/tarifflist'
BARCODE_ENDPOINT = 'print/barcodes'
WEBHOOK_ENDPOINT = 'webhooks'
MAX_RETRIES = 2
@ -204,8 +217,26 @@ class CDEKClient:
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)
if not is_migration_running():
client.authorize()
client.setup_webhooks()

View File

@ -36,10 +36,12 @@ def get_secret(setting):
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_secret("SECRET_KEY")
SITE_URL = get_secret("SITE_URL")
# External API settings
CDEK_CLIENT_ID = get_secret("CDEK_CLIENT_ID")
CDEK_CLIENT_SECRET = get_secret("CDEK_CLIENT_SECRET")
CDEK_WEBHOOK_URL_SALT = get_secret("CDEK_WEBHOOK_URL_SALT")
POIZON_TOKEN = get_secret("POIZON_TOKEN")

View File

@ -1,6 +1,13 @@
from functools import reduce
from rest_framework.fields import DecimalField
class PriceField(DecimalField):
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)
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)

View File

@ -9,6 +9,8 @@ from .models import Checklist, GlobalSettings
@shared_task
def check_cdek_status(order_id):
"""Manually check CDEK status of order"""
obj = Checklist.objects.filter(id=order_id).first()
if obj is None or obj.cdek_tracking is None or obj.status == Checklist.Status.COMPLETED:
return
@ -20,6 +22,7 @@ def check_cdek_status(order_id):
old_status = obj.status
new_status = obj.status
if CDEKStatus.DELIVERED in statuses:
new_status = Checklist.Status.COMPLETED
elif CDEKStatus.READY_FOR_SHIPMENT_IN_SENDER_CITY in statuses:

View File

@ -13,7 +13,7 @@ from rest_framework.generics import get_object_or_404
from rest_framework.permissions import AllowAny, IsAuthenticated
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 utils.exceptions import CRMException
from store.filters import GiftFilter, ChecklistFilter
@ -350,6 +350,35 @@ class CDEKAPI(viewsets.GenericViewSet):
r = self.client.calculate_tarifflist(data)
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
class PoizonAPI(viewsets.GenericViewSet):