From 2e79e579f7b20d81ef42fb2ec9505fb8f81dd398 Mon Sep 17 00:00:00 2001 From: phzhik Date: Thu, 23 Nov 2023 02:16:53 +0400 Subject: [PATCH] + CurrencyAPIClient for fresh CNY rate + yuan_rate_last_updated, yuan_rate_commission fields in GlobalSettings --- external_api/currency.py | 57 +++++++++++++++++++ poizonstore/settings.py | 3 + ...lsettings_yuan_rate_commission_and_more.py | 23 ++++++++ store/models.py | 29 +++++++++- store/serializers.py | 4 +- store/views.py | 2 +- 6 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 external_api/currency.py create mode 100644 store/migrations/0048_globalsettings_yuan_rate_commission_and_more.py diff --git a/external_api/currency.py b/external_api/currency.py new file mode 100644 index 0000000..cc88bd0 --- /dev/null +++ b/external_api/currency.py @@ -0,0 +1,57 @@ +from decimal import Decimal +from contextlib import suppress +from urllib.parse import urljoin + +import requests +from django.conf import settings + + +class CurrencyAPIClient: + CONVERT_ENDPOINT = 'currency/convert' + + MAX_RETRIES = 2 + + def __init__(self, api_key: str): + self.api_url = 'https://api.getgeoapi.com/v2/' + self.api_key = api_key + self.session = requests.Session() + + def request(self, method, url, *args, **kwargs): + params = kwargs.pop('params', {}) + params.update({"api_key": self.api_key}) + + joined_url = urljoin(self.api_url, url) + request = requests.Request(method, joined_url, params=params, *args, **kwargs) + + retries = 0 + while retries < self.MAX_RETRIES: + retries += 1 + prepared = self.session.prepare_request(request) + r = self.session.send(prepared) + + # TODO: handle/log errors + return r + + def get_rate(self, currency1: str, currency2: str, amount=1): + params = { + 'from': currency1, + 'to': currency2, + 'amount': amount, + 'format': 'json' + } + + r = self.request('GET', self.CONVERT_ENDPOINT, params=params) + if not r or r.json().get('status') == 'failed': + return None + + with suppress(KeyError): + rate = r.json()['rates'][currency2.upper()]['rate'] + return Decimal(rate) + + return None + + def get_cny_rate(self): + return self.get_rate('cny', 'rub') + + +client = CurrencyAPIClient(settings.CURRENCY_GETGEOIP_API_KEY) diff --git a/poizonstore/settings.py b/poizonstore/settings.py index 10660e6..196e616 100644 --- a/poizonstore/settings.py +++ b/poizonstore/settings.py @@ -29,6 +29,8 @@ CDEK_CLIENT_SECRET = 'lc2gmrmK5s1Kk6FhZbNqpQCaATQRlsOy' POIZON_TOKEN = 'IRwNgBxb8YQ' +CURRENCY_GETGEOIP_API_KEY = '136a69df7e4f419c97783288068e5a2f1ef4ad6d' + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = bool(int(os.environ.get("DJANGO_DEBUG") or 0)) DISABLE_PERMISSIONS = False @@ -198,6 +200,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' CHECKLIST_ID_LENGTH = 10 COMMISSION_OVER_150K = 1.1 +YUAN_RATE_UPDATE_PERIOD_MINUTES = 60 # Logging SENTRY_DSN = "https://96106e3f938badc86ecb2e502716e496@o4506163299418112.ingest.sentry.io/4506163300663296" diff --git a/store/migrations/0048_globalsettings_yuan_rate_commission_and_more.py b/store/migrations/0048_globalsettings_yuan_rate_commission_and_more.py new file mode 100644 index 0000000..25578d8 --- /dev/null +++ b/store/migrations/0048_globalsettings_yuan_rate_commission_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.2 on 2023-11-22 22:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('store', '0047_alter_checklist_gift'), + ] + + operations = [ + migrations.AddField( + model_name='globalsettings', + name='yuan_rate_commission', + field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Наценка на курс юаня, руб'), + ), + migrations.AddField( + model_name='globalsettings', + name='yuan_rate_last_updated', + field=models.DateTimeField(default=None, null=True, verbose_name='Дата обновления курса CNY/RUB'), + ), + ] diff --git a/store/models.py b/store/models.py index e85e135..fb92096 100644 --- a/store/models.py +++ b/store/models.py @@ -23,6 +23,7 @@ from django_cleanup import cleanup from mptt.fields import TreeForeignKey from mptt.models import MPTTModel +from external_api.currency import client as CurrencyAPIClient from store.utils import create_preview, concat_not_null_values from utils.cache import InMemoryCache @@ -30,6 +31,8 @@ from utils.cache import InMemoryCache class GlobalSettings(models.Model): # currency yuan_rate = models.DecimalField('Курс CNY/RUB', max_digits=10, decimal_places=2, default=0) + yuan_rate_last_updated = models.DateTimeField('Дата обновления курса CNY/RUB', null=True, default=None) + yuan_rate_commission = models.DecimalField('Наценка на курс юаня, руб', max_digits=10, decimal_places=2, default=0) # Chinadelivery delivery_price_CN = models.DecimalField('Цена доставки по Китаю', max_digits=10, decimal_places=2, default=0) commission_rub = models.DecimalField('Комиссия, руб', max_digits=10, decimal_places=2, default=0) @@ -63,6 +66,26 @@ class GlobalSettings(models.Model): InMemoryCache.set('GlobalSettings', obj) return obj + def get_yuan_rate(self): + """ Get rate either from CurrencyAPI or from DB (if fresh enough) """ + if self.yuan_rate_last_updated is not None: + diff_minutes = (timezone.now() - self.yuan_rate_last_updated).total_seconds() / 60 + else: + diff_minutes = None + + if diff_minutes is None or diff_minutes > settings.YUAN_RATE_UPDATE_PERIOD_MINUTES: + # Get fresh rate from API + rate = CurrencyAPIClient.get_cny_rate() + if rate: + # Save rate in DB for future usage + self.yuan_rate = rate + self.yuan_rate_last_updated = timezone.now() + self.save() + return rate + + # Default + return self.yuan_rate + class Category(MPTTModel): name = models.CharField('Название', max_length=20) @@ -271,7 +294,7 @@ class ChecklistQuerySet(models.QuerySet): return self.annotate( _yuan_rate=Case( When(price_snapshot_id__isnull=False, then=F('price_snapshot__yuan_rate')), - default=GlobalSettings.load().yuan_rate + default=GlobalSettings.load().get_yuan_rate() ), _price_rub=Ceil(F('_yuan_rate') * F('price_yuan')) ) @@ -448,7 +471,7 @@ class Checklist(models.Model): if self.price_snapshot_id: yuan_rate = self.price_snapshot.yuan_rate else: - yuan_rate = GlobalSettings.load().yuan_rate + yuan_rate = GlobalSettings.load().get_yuan_rate() return math.ceil(yuan_rate * self.price_yuan) @@ -483,7 +506,7 @@ class Checklist(models.Model): if self.price_snapshot_id: return self.price_snapshot.yuan_rate else: - return GlobalSettings.load().yuan_rate + return GlobalSettings.load().get_yuan_rate() @property def delivery_price_CN(self) -> Decimal: diff --git a/store/serializers.py b/store/serializers.py index 2e4dd0f..13247fd 100644 --- a/store/serializers.py +++ b/store/serializers.py @@ -206,14 +206,14 @@ class AnonymousUserChecklistSerializer(ChecklistSerializer): class GlobalSettingsSerializer(serializers.ModelSerializer): - currency = serializers.DecimalField(source='yuan_rate', max_digits=10, decimal_places=2) + currency = serializers.DecimalField(source='get_yuan_rate', read_only=True, max_digits=10, decimal_places=2) chinadelivery = serializers.DecimalField(source='delivery_price_CN', max_digits=10, decimal_places=2) commission = serializers.DecimalField(source='commission_rub', max_digits=10, decimal_places=2) pickup = serializers.CharField(source='pickup_address') class Meta: model = GlobalSettings - fields = ('currency', 'commission', 'chinadelivery', 'pickup', 'time_to_buy') + fields = ('currency', 'yuan_rate_commission', 'commission', 'chinadelivery', 'pickup', 'time_to_buy') class PaymentMethodSerializer(serializers.ModelSerializer): diff --git a/store/views.py b/store/views.py index e527bc5..4f8fd6b 100644 --- a/store/views.py +++ b/store/views.py @@ -202,7 +202,7 @@ class StatisticsAPI(viewsets.GenericViewSet): @action(url_path='orders', detail=False, methods=['get']) def stats_by_orders(self, request, *args, **kwargs): global_settings = GlobalSettings.load() - yuan_rate = global_settings.yuan_rate + yuan_rate = global_settings.get_yuan_rate() # Prepare query to collect the stats qs = self.get_queryset() \