diff --git a/.env.template b/.env.template index 72365b5..1e77541 100644 --- a/.env.template +++ b/.env.template @@ -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="" diff --git a/external_api/cdek.py b/external_api/cdek.py index f7ed7c0..75fe7f8 100644 --- a/external_api/cdek.py +++ b/external_api/cdek.py @@ -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() diff --git a/poizonstore/settings.py b/poizonstore/settings.py index c3d25f3..ef7ccfb 100644 --- a/poizonstore/settings.py +++ b/poizonstore/settings.py @@ -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") diff --git a/poizonstore/utils.py b/poizonstore/utils.py index e411fb0..209ac2b 100644 --- a/poizonstore/utils.py +++ b/poizonstore/utils.py @@ -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) diff --git a/store/tasks.py b/store/tasks.py index ce219b7..c67c4b1 100644 --- a/store/tasks.py +++ b/store/tasks.py @@ -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: diff --git a/store/views.py b/store/views.py index aa3e934..1b51073 100644 --- a/store/views.py +++ b/store/views.py @@ -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):