From 3320ff93e3a7c3d025990d20985ae81396aa6fe9 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 7 Jul 2026 12:10:19 +0330 Subject: [PATCH 1/4] temp --- apps/promotions/tests.py | 94 +++++++++++++++++++++++++++- apps/promotions/urls_application.py | 1 + apps/promotions/views_application.py | 27 +++++++- main/urls.py | 2 +- utils/clients/accounts_client.py | 76 ++++++++++++++++++++++ 5 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 utils/clients/accounts_client.py diff --git a/apps/promotions/tests.py b/apps/promotions/tests.py index 736bdf6..7d7378a 100644 --- a/apps/promotions/tests.py +++ b/apps/promotions/tests.py @@ -611,10 +611,102 @@ class ApplicationApiFlowsTests(APITestCase): self.first_ad_view_reward_plan.refresh_from_db() self.assertEqual(self.first_ad_view_reward_plan.balance, 10000 - 800) - # response = self.client.get(reverse('promotions:promotion-list', kwargs={"plan": self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, # format='json') # print(response.json()) # response = self.client.post(reverse('promotions:promotion-create', kwargs={"plan": self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, format='json') # print(response.json()) + + def test_application_promote_referral_reward_success(self): + self.referral_reward_event_saver, _created = EventSaver.objects.get_or_create( + title=self.referral_reward_label, + event_label=self.referral_reward_label, + save_once=True, + ) + + self.referral_reward_plan, _created = Plan.objects.get_or_create( + title=self.referral_reward_label, + balance=10000, + user=self.user_1, + application=self.application, + event_list=[self.referral_reward_label] + ) + + self.referral_reward_recipient, _created = Recipient.objects.get_or_create( + label=self.referral_reward_label, + plan=self.referral_reward_plan, + wallet_uuid=self.user_1_uuid, + recipient_uuid_field="->event:referral", + base_amount_field="1000", + + ) + + auth = self._create_authorization_header(self.application_access_token.token) + event_create_data = { + 'label': self.referral_reward_label, + "data": { + 'user': self.user_1_uuid, + 'referral': self.user_2_uuid + } + } + url = reverse('promotions-application:plan-promote', + kwargs={"pk": self.referral_reward_plan.uuid, "user_uuid": self.user_1_uuid}) + response = self.client.post(url, event_create_data, HTTP_AUTHORIZATION=auth, format='json') + print(response.json()) + self.referral_reward_plan.refresh_from_db() + self.assertEqual(self.referral_reward_plan.balance, 10000 - 1000) + self.assertEqual(response.json()['promotions'][0]['user'], str(self.user_2_uuid)) + + def test_application_get_promotion_plan_success(self): + self.first_ad_view_reward_event_saver, _created = EventSaver.objects.get_or_create( + title=self.first_ad_view_reward_label, + event_label=self.first_ad_view_reward_label, + save_once=True, + ) + + self.first_ad_view_reward_plan, _created = Plan.objects.get_or_create( + title=self.first_ad_view_reward_label, + balance=10000, + user=self.user_1, + application=self.application, + event_list=[self.first_ad_view_reward_label] + ) + + self.first_ad_view_reward_recipient, _created = Recipient.objects.get_or_create( + label=self.first_ad_view_reward_label, + plan=self.first_ad_view_reward_plan, + wallet_uuid=self.user_1_uuid, + recipient_uuid_field="->event:user", + base_amount_field="event:base_amount", + + ) + + auth = self._create_authorization_header(self.application_access_token.token) + event_create_data = { + 'label': self.first_ad_view_reward_label, + "data": { + 'user': self.user_1_uuid, + 'base_amount': 800 + } + } + + url_promote = reverse('promotions-application:plan-promote', + kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": self.user_1_uuid}) + response = self.client.post(url_promote, event_create_data, HTTP_AUTHORIZATION=auth, format='json') + print(response.json()) + + self.first_ad_view_reward_plan.refresh_from_db() + self.assertEqual(self.first_ad_view_reward_plan.balance, 10000 - 800) + + + # url_get_promotion = reverse('promotions-application:plan-list', + # kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": self.user_1_uuid}) + url_get_promotion = reverse('promotions-application:plan-list', + kwargs={"user_uuid": self.user_1_uuid}) + + + response = self.client.get(url_get_promotion, HTTP_AUTHORIZATION=auth, format='json') + print(response.json()) + + diff --git a/apps/promotions/urls_application.py b/apps/promotions/urls_application.py index 4e6f095..40213fb 100644 --- a/apps/promotions/urls_application.py +++ b/apps/promotions/urls_application.py @@ -54,6 +54,7 @@ class ApplicationRouter(DefaultRouter): router = DefaultRouter() +router.register('/plan', views_application.ApplicationUserPlanViewSet, basename='plan') router.register('plan', views_application.ApplicationPlanViewSet, basename='plan') router.register('event', views_application.ApplicationEventViewSet, basename='event') diff --git a/apps/promotions/views_application.py b/apps/promotions/views_application.py index 0e05d62..a4fb7fc 100644 --- a/apps/promotions/views_application.py +++ b/apps/promotions/views_application.py @@ -16,13 +16,13 @@ from .tasks import analyze_event_task from ..users.models import User -class ApplicationPlanViewSet( +class ApplicationUserPlanViewSet( mixins.RetrieveModelMixin, mixins.ListModelMixin, # mixins.CreateModelMixin, GenericViewSet ): - serializer_class = PromotionSerializer + serializer_class = UserPlanSerializer permission_classes = [TokenMatchesViewSetActions] required_action_scopes = { @@ -33,6 +33,9 @@ class ApplicationPlanViewSet( def get_queryset(self): application = get_application(self.request) + # TODO: get user from accounts + + user_uuid = self.kwargs.get('user_uuid') # return Promotion.objects.filter(application=application) return Plan.objects.all() @@ -61,7 +64,7 @@ class ApplicationPlanViewSet( # TODO # try: - user_uuid = self.kwargs.get('user_uuid') + event = event_saver.save_event(user=user_uuid, application=application, **serializer.validated_data) # except Exception as e: @@ -109,6 +112,24 @@ class ApplicationEventViewSet( analyze_event_task.delay(event.uuid) serializer.instance = event +class ApplicationPlanViewSet( + GenericViewSet +): + serializer_class = PlanSerializer + + permission_classes = [TokenMatchesViewSetActions] + required_action_scopes = { + "retrieve": [["promotions.application.plan:list-retrieve"]], + "list": [["promotions.application.plan:list-retrieve"]], + } + + def get_queryset(self): + application = get_application(self.request) + # return Promotion.objects.filter(application=application) + return Plan.objects.all() + + + # NOT class UserPlanViewSet(mixins.RetrieveModelMixin, diff --git a/main/urls.py b/main/urls.py index 1431bb6..ff2dddb 100644 --- a/main/urls.py +++ b/main/urls.py @@ -29,7 +29,7 @@ urlpatterns = [ path('', include('utils.urls')), path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')), path('promotions/', include('apps.promotions.urls_user', namespace='promotions')), - path('api/v2/promotions/application//', include('apps.promotions.urls_application', namespace='promotions-application')), + path('api/v2/promotions/application/', include('apps.promotions.urls_application', namespace='promotions-application')), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/utils/clients/accounts_client.py b/utils/clients/accounts_client.py new file mode 100644 index 0000000..f63a4bd --- /dev/null +++ b/utils/clients/accounts_client.py @@ -0,0 +1,76 @@ +import time + +import requests +from django.conf import settings +from django.core.cache import cache + +from gooyal_accounts_client import AuthenticatedClient +from gooyal_accounts_client.models import PublicUser, Application +from gooyal_accounts_client.api.users import users_api_users_retrieve +from gooyal_accounts_client.api.oauth2 import oauth2_apis_applications_retrieve +from gooyal_accounts_client.models import Account +from gooyal_accounts_client.api.users import (users_api_users_details_retrieve) + + +def login_as_client_credentials(): + access_token = cache.get('wallet_access_token') + # expires_in = self.auth_data.get('expires_in') + # created_at = self.auth_data.get('created_at') + # + # if access_token and expires_in > time.time() - created_at: + # return self.auth_data + if access_token: + print('already in cache') + return access_token + + print('not in cache') + + data = { + "grant_type": 'client_credentials', + "scope": settings.SCOPES, + } + auth = (settings.CLIENT_ID, settings.CLIENT_SECRET) + + response = requests.post(f'{settings.BASE_OAUTH2_PROVIDER_PUBLIC_URL}/token/', + data=data, + auth=auth) + + auth_data = response.json() + + print(auth_data) + + if 'access_token' in auth_data: + auth_data['created_at'] = time.time() + cache.set('wallet_access_token', auth_data, timeout=auth_data.get('expires_in')-5) + + return auth_data + +def get_client(): + access_token = login_as_client_credentials()['access_token'] + # TODO: url + client = AuthenticatedClient(base_url="http://accounts:8000", token=access_token) + return client + + +def get_user_details(uuid): + client = get_client() + data: PublicUser = users_api_users_retrieve.sync(client=client, uuid=uuid) + print(data) + # or if you need more info (e.g. status_code) + # response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client) + + return data + +def get_user_detailed_info(uuid): + client = get_client() + response: Account = users_api_users_details_retrieve.sync(uuid=uuid, client=client) + print(response) + + return response + +def get_application_details(uuid): + client = get_client() + data: Application = oauth2_apis_applications_retrieve.sync(client=client, id=uuid) + print(data) + + return data From e7db91d1da56c665912821c1b3a03f7f28157244 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 7 Jul 2026 16:42:34 +0330 Subject: [PATCH 2/4] get user from accounts --- apps/promotions/tests.py | 24 ++++++------- apps/promotions/urls_application.py | 4 +-- apps/promotions/views_application.py | 13 +++++-- main/settings.py | 2 ++ main/urls.py | 2 +- utils/clients/accounts_client.py | 53 +++++++++++++++++----------- utils/clients/wallet_client.py | 26 ++++++++------ 7 files changed, 76 insertions(+), 48 deletions(-) diff --git a/apps/promotions/tests.py b/apps/promotions/tests.py index 7d7378a..5846a18 100644 --- a/apps/promotions/tests.py +++ b/apps/promotions/tests.py @@ -65,7 +65,7 @@ class ApplicationApiFlowsTests(APITestCase): application_uuid = uuid.UUID(application_uuid_hex) application_access_token = '14naVsdKCbKNOhnElPyXcrwSnqqFbm' user_access_token = '24naVsdKCbKNOhnElPyXcrwSnqqFbm' - user_1_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470051') + user_1_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f') user_2_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470052') first_ad_view_reward_label = 'first_ad_view_reward_label' referral_reward_label = 'referral_reward_label' @@ -650,8 +650,12 @@ class ApplicationApiFlowsTests(APITestCase): 'referral': self.user_2_uuid } } - url = reverse('promotions-application:plan-promote', - kwargs={"pk": self.referral_reward_plan.uuid, "user_uuid": self.user_1_uuid}) + url = reverse('promotions-application:application-user-plan-promote', + kwargs={ + "pk": str(self.referral_reward_plan.uuid), + "user_uuid": str(self.user_1_uuid) + } + ) response = self.client.post(url, event_create_data, HTTP_AUTHORIZATION=auth, format='json') print(response.json()) self.referral_reward_plan.refresh_from_db() @@ -691,22 +695,18 @@ class ApplicationApiFlowsTests(APITestCase): } } - url_promote = reverse('promotions-application:plan-promote', - kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": self.user_1_uuid}) + url_promote = reverse('promotions-application:application-user-plan-promote', + kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": '1f9c72e4-3cc3-420d-928b-b0a3d65a4612'}) response = self.client.post(url_promote, event_create_data, HTTP_AUTHORIZATION=auth, format='json') - print(response.json()) + # print(response.json()) self.first_ad_view_reward_plan.refresh_from_db() self.assertEqual(self.first_ad_view_reward_plan.balance, 10000 - 800) - # url_get_promotion = reverse('promotions-application:plan-list', # kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": self.user_1_uuid}) - url_get_promotion = reverse('promotions-application:plan-list', - kwargs={"user_uuid": self.user_1_uuid}) - + url_get_promotion = reverse('promotions-application:application-user-plan-detail', + kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": '1f9c72e4-3cc3-420d-928b-b0a3d65a4612'}) response = self.client.get(url_get_promotion, HTTP_AUTHORIZATION=auth, format='json') print(response.json()) - - diff --git a/apps/promotions/urls_application.py b/apps/promotions/urls_application.py index 40213fb..6a0121b 100644 --- a/apps/promotions/urls_application.py +++ b/apps/promotions/urls_application.py @@ -54,8 +54,8 @@ class ApplicationRouter(DefaultRouter): router = DefaultRouter() -router.register('/plan', views_application.ApplicationUserPlanViewSet, basename='plan') -router.register('plan', views_application.ApplicationPlanViewSet, basename='plan') +router.register('plan', views_application.ApplicationUserPlanViewSet, basename='application-user-plan') +# router.register('plan', views_application.ApplicationPlanViewSet, basename='application-plan') router.register('event', views_application.ApplicationEventViewSet, basename='event') urlpatterns = router.urls diff --git a/apps/promotions/views_application.py b/apps/promotions/views_application.py index a4fb7fc..89da921 100644 --- a/apps/promotions/views_application.py +++ b/apps/promotions/views_application.py @@ -9,6 +9,7 @@ from rest_framework.viewsets import GenericViewSet from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements, TokenMatchesViewSetActions from apps.gooyal_oauth2.utils import get_application +from utils.clients.accounts_client import get_user_info from utils.exceptions import UnprocessableEntity from .models import Plan, Promotion, EventSaver from .serializers import PlanSerializer, PromotionSerializer, EventSerializer, PromoteSerializer, UserPlanSerializer @@ -35,7 +36,15 @@ class ApplicationUserPlanViewSet( application = get_application(self.request) # TODO: get user from accounts - user_uuid = self.kwargs.get('user_uuid') + if not self.request.user: + user_uuid = self.kwargs.get('user_uuid') + user = User.objects.filter(uuid=user_uuid).first() + if not user: + user_info = get_user_info(user_uuid) + if user_info: + user, created = User.objects.update_or_create(uuid=user_uuid) + + self.request.user = user # return Promotion.objects.filter(application=application) return Plan.objects.all() @@ -65,7 +74,7 @@ class ApplicationUserPlanViewSet( # TODO # try: - event = event_saver.save_event(user=user_uuid, application=application, **serializer.validated_data) + event = event_saver.save_event(user=self.request.user.pk, application=application, **serializer.validated_data) # except Exception as e: # raise exceptions.ValidationError(str(e)) diff --git a/main/settings.py b/main/settings.py index be5a728..ddc2513 100644 --- a/main/settings.py +++ b/main/settings.py @@ -382,6 +382,8 @@ CELERY_TASK_TIME_LIMIT = 30 * 60 CELERY_BROKER_URL = REDIS_BASE_URL CELERY_RESULT_BACKEND = REDIS_BASE_URL +ACCOUNTS_BASE_PUBLIC_URL = config('ACCOUNTS_BASE_PUBLIC_URL', default=None, cast=str) + WALLET_BASE_PUBLIC_URL = config('WALLET_BASE_PUBLIC_URL', default=None, cast=str) WALLET_RIAL = config('WALLET_RIAL', cast=str) WALLET_REWARD = config('WALLET_REWARD', cast=str) diff --git a/main/urls.py b/main/urls.py index ff2dddb..1431bb6 100644 --- a/main/urls.py +++ b/main/urls.py @@ -29,7 +29,7 @@ urlpatterns = [ path('', include('utils.urls')), path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')), path('promotions/', include('apps.promotions.urls_user', namespace='promotions')), - path('api/v2/promotions/application/', include('apps.promotions.urls_application', namespace='promotions-application')), + path('api/v2/promotions/application//', include('apps.promotions.urls_application', namespace='promotions-application')), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/utils/clients/accounts_client.py b/utils/clients/accounts_client.py index f63a4bd..c7726de 100644 --- a/utils/clients/accounts_client.py +++ b/utils/clients/accounts_client.py @@ -3,17 +3,18 @@ import time import requests from django.conf import settings from django.core.cache import cache +from httpx import Request -from gooyal_accounts_client import AuthenticatedClient -from gooyal_accounts_client.models import PublicUser, Application -from gooyal_accounts_client.api.users import users_api_users_retrieve -from gooyal_accounts_client.api.oauth2 import oauth2_apis_applications_retrieve -from gooyal_accounts_client.models import Account -from gooyal_accounts_client.api.users import (users_api_users_details_retrieve) +from utils.clients.gooyal_accounts_client import AuthenticatedClient +from utils.clients.gooyal_accounts_client.models import PublicUser, Application +from utils.clients.gooyal_accounts_client.api.users import users_api_users_retrieve +from utils.clients.gooyal_accounts_client.api.oauth2 import oauth2_apis_applications_retrieve +from utils.clients.gooyal_accounts_client.models import Account +from utils.clients.gooyal_accounts_client.api.users import (users_api_users_details_retrieve) def login_as_client_credentials(): - access_token = cache.get('wallet_access_token') + access_token = cache.get('promotions_access_token') # expires_in = self.auth_data.get('expires_in') # created_at = self.auth_data.get('created_at') # @@ -27,39 +28,50 @@ def login_as_client_credentials(): data = { "grant_type": 'client_credentials', - "scope": settings.SCOPES, + "scope": settings.OAUTH2_SCOPES, } - auth = (settings.CLIENT_ID, settings.CLIENT_SECRET) + auth = (settings.OAUTH2_CLIENT_ID, settings.OAUTH2_CLIENT_SECRET) - response = requests.post(f'{settings.BASE_OAUTH2_PROVIDER_PUBLIC_URL}/token/', + response = requests.post(f'{settings.OAUTH2_PROVIDER_PUBLIC_URL}/token/', data=data, auth=auth) auth_data = response.json() print(auth_data) - if 'access_token' in auth_data: auth_data['created_at'] = time.time() - cache.set('wallet_access_token', auth_data, timeout=auth_data.get('expires_in')-5) + cache.set('promotions_access_token', auth_data, timeout=auth_data.get('expires_in') - 5) return auth_data + +def log_request(request: Request): + print(request.headers['authorization']) + print( + f"Request event hook: {request.method} {request.url} {request.method} {request.headers} - Waiting for response") + + +def log_response(response): + request = response.request + print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + + def get_client(): access_token = login_as_client_credentials()['access_token'] - # TODO: url - client = AuthenticatedClient(base_url="http://accounts:8000", token=access_token) + client = AuthenticatedClient(base_url=settings.ACCOUNTS_BASE_PUBLIC_URL, token=access_token, + httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}} + ) return client -def get_user_details(uuid): +def get_user_info(uuid): client = get_client() - data: PublicUser = users_api_users_retrieve.sync(client=client, uuid=uuid) - print(data) - # or if you need more info (e.g. status_code) - # response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client) + response: PublicUser = users_api_users_retrieve.sync(client=client, uuid=uuid) + print(response) + + return response - return data def get_user_detailed_info(uuid): client = get_client() @@ -68,6 +80,7 @@ def get_user_detailed_info(uuid): return response + def get_application_details(uuid): client = get_client() data: Application = oauth2_apis_applications_retrieve.sync(client=client, id=uuid) diff --git a/utils/clients/wallet_client.py b/utils/clients/wallet_client.py index cd63903..4cf8934 100644 --- a/utils/clients/wallet_client.py +++ b/utils/clients/wallet_client.py @@ -6,7 +6,8 @@ from django.core.cache import cache from httpx import Request from utils.clients.gooyal_wallet_client import AuthenticatedClient -from utils.clients.gooyal_wallet_client.models import Transaction, Account, ApplicationDeposit, ApplicationWithdraw, PaginatedAccountList +from utils.clients.gooyal_wallet_client.models import Transaction, Account, ApplicationDeposit, ApplicationWithdraw, \ + PaginatedAccountList from utils.clients.gooyal_wallet_client.api.wallet import (wallet_api_application_deposit_create, wallet_api_application_deposit_verify_retrieve, wallet_api_application_withdraw_create, @@ -14,10 +15,8 @@ from utils.clients.gooyal_wallet_client.api.wallet import (wallet_api_applicatio wallet_api_application_user_wallets_list) - - def login_as_client_credentials(): - access_token = cache.get('settlement_access_token') + access_token = cache.get('promotions_access_token') # expires_in = self.auth_data.get('expires_in') # created_at = self.auth_data.get('created_at') # @@ -38,12 +37,13 @@ def login_as_client_credentials(): response = requests.post(f'{settings.OAUTH2_PROVIDER_PUBLIC_URL}/token/', data=data, auth=auth) + print(response.content) auth_data = response.json() if 'access_token' in auth_data: auth_data['created_at'] = time.time() - cache.set('settlement_access_token', auth_data, timeout=auth_data.get('expires_in') - 5) + cache.set('promotions_access_token', auth_data, timeout=auth_data.get('expires_in') - 5) return auth_data @@ -75,15 +75,16 @@ def deposit_to_user_wallet_submit(payer_wallet, data): deposit_data = Tmp() client = get_client() response: ApplicationDeposit = wallet_api_application_deposit_create.sync(payer_wallet=payer_wallet, - client=client, - body=deposit_data) + client=client, + body=deposit_data) print(response) return response def deposit_to_user_wallet_verify(payer_wallet, uuid): client = get_client() - response: ApplicationDeposit = wallet_api_application_deposit_verify_retrieve.sync(payer_wallet=payer_wallet, uuid=uuid, client=client) + response: ApplicationDeposit = wallet_api_application_deposit_verify_retrieve.sync(payer_wallet=payer_wallet, + uuid=uuid, client=client) print(response) return response @@ -95,14 +96,16 @@ def withdraw_from_user_wallet_submit(payee_wallet, data): withdraw_data = Tmp() client = get_client() - response: ApplicationWithdraw = wallet_api_application_withdraw_create.sync(payee_wallet=payee_wallet, client=client, body=withdraw_data) + response: ApplicationWithdraw = wallet_api_application_withdraw_create.sync(payee_wallet=payee_wallet, + client=client, body=withdraw_data) print(response) return response def withdraw_from_user_wallet_verify(payee_wallet, uuid): client = get_client() - response: ApplicationWithdraw = wallet_api_application_withdraw_verify_retrieve.sync(payee_wallet=payee_wallet, uuid=uuid, client=client) + response: ApplicationWithdraw = wallet_api_application_withdraw_verify_retrieve.sync(payee_wallet=payee_wallet, + uuid=uuid, client=client) print(response) return response @@ -110,7 +113,8 @@ def withdraw_from_user_wallet_verify(payee_wallet, uuid): def get_user_wallets(owner, wallets): client = get_client() # by pass several wallet query params: [','.join([str(i) for i in wallets])] - response: PaginatedAccountList = wallet_api_application_user_wallets_list.sync(owner=owner, wallet=[','.join([str(i) for i in wallets])], client=client) + response: PaginatedAccountList = wallet_api_application_user_wallets_list.sync(owner=owner, wallet=[ + ','.join([str(i) for i in wallets])], client=client) print(response) balances = dict(map(lambda result: (str(result.wallet), result.balance), response.results)) return balances From 1eca11e4a488254757d3cb9d3e1824a4050ebc0d Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 7 Jul 2026 17:24:32 +0330 Subject: [PATCH 3/4] better exceptions --- apps/promotions/views_application.py | 7 +++- utils/exceptions.py | 55 ++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/apps/promotions/views_application.py b/apps/promotions/views_application.py index 89da921..becd075 100644 --- a/apps/promotions/views_application.py +++ b/apps/promotions/views_application.py @@ -2,6 +2,7 @@ from drf_spectacular.utils import extend_schema from rest_framework import mixins, status from rest_framework.decorators import action from rest_framework import exceptions +from rest_framework.exceptions import NotFound from rest_framework.generics import CreateAPIView, get_object_or_404, RetrieveAPIView, ListAPIView from rest_framework.response import Response from rest_framework.settings import api_settings @@ -15,6 +16,7 @@ from .models import Plan, Promotion, EventSaver from .serializers import PlanSerializer, PromotionSerializer, EventSerializer, PromoteSerializer, UserPlanSerializer from .tasks import analyze_event_task from ..users.models import User +from django.utils.translation import gettext as _ class ApplicationUserPlanViewSet( @@ -42,7 +44,10 @@ class ApplicationUserPlanViewSet( if not user: user_info = get_user_info(user_uuid) if user_info: - user, created = User.objects.update_or_create(uuid=user_uuid) + user, created = User.objects.get_or_create(uuid=user_uuid) + + if not user: + raise NotFound(_('user {user_uuid} not found').format(user_uuid=user_uuid)) self.request.user = user # return Promotion.objects.filter(application=application) diff --git a/utils/exceptions.py b/utils/exceptions.py index 807f9b5..27f7b07 100644 --- a/utils/exceptions.py +++ b/utils/exceptions.py @@ -1,5 +1,7 @@ import logging +from django.conf import settings +from django.utils import timezone from rest_framework.exceptions import APIException from rest_framework.views import exception_handler as drf_exception_handler @@ -12,17 +14,36 @@ def exception_handler(exc, context): response = drf_exception_handler(exc, context) if response is not None: - # ساختار دلخواه خود را تعریف می‌کنیم response_data = { 'success': False, 'status_code': response.status_code, - 'message': 'An error occurred', - 'details': {} + 'status_message': str(getattr(exc, 'default_detail', exc)), + 'details': getattr(exc, 'detail', None), } if isinstance(exc, APIException): - response_data['message'] = exc.detail if isinstance(exc.detail, str) else "Validation error" - response_data['details'] = exc.detail if isinstance(exc.detail, dict) else {} + try: + error = getattr(getattr(exc, 'detail', None), 'code', None) or getattr(exc, 'code', None) + if not error: + error = getattr(exc, 'default_code') + except: + error = '' + + # if isinstance(exc.detail, str): + # message = exc.detail + # elif isinstance(exc.detail, dict): + # if 'message' in exc.detail or 'string' in exc.detail: + # message = exc.detail.get('message') or exc.detail.get('string') + # else: + # message = error + # + # else: + # message = error + # + # response_data['message'] = message + response_data['details'] = {"message": exc.detail} + response_data['details']['error'] = error + response_data['details']['timestamp'] = timezone.now().isoformat() response.data = response_data @@ -47,16 +68,36 @@ class ErrorMiddleware: { "success": False, "status_code": 500, - "message": "Internal server error", - "details": str(exception) # فقط در حالت توسعه! در تولید بهتر است لاگ شود. + "status_message": "Internal server error", + "details": str(exception) if settings.DEBUG else None, }, status=500 ) from rest_framework.exceptions import APIException +from rest_framework import status +from django.utils.translation import gettext_lazy as _ + class UnprocessableEntity(APIException): status_code = 422 default_detail = 'The request was well-formed but cannot be processed due to semantic errors.' default_code = 'unprocessable_entity' + + +class ServiceUnavailable(APIException): + status_code = status.HTTP_503_SERVICE_UNAVAILABLE + default_ = _('SERVICE_UNAVAILABLE') + default_code = 'service_unavailable' + default_detail = 'Service Unavailable' + + +class Conflict(APIException): + status_code = status.HTTP_409_CONFLICT + default_ = _('CONFLICT') + default_code = 'conflict' + default_detail = 'Conflict' + + +# class NotFound(APIException): From a8b10b79a59b44b67fcdfec736ce36e8b80d70da Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Wed, 8 Jul 2026 12:05:42 +0330 Subject: [PATCH 4/4] user availability test --- apps/promotions/tests.py | 33 ++++++++++++++++++---------- apps/promotions/views_application.py | 2 -- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/apps/promotions/tests.py b/apps/promotions/tests.py index 5846a18..d7c8c12 100644 --- a/apps/promotions/tests.py +++ b/apps/promotions/tests.py @@ -696,17 +696,26 @@ class ApplicationApiFlowsTests(APITestCase): } url_promote = reverse('promotions-application:application-user-plan-promote', - kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": '1f9c72e4-3cc3-420d-928b-b0a3d65a4612'}) + kwargs={"pk": self.first_ad_view_reward_plan.uuid, + "user_uuid": '1f9c72e4-3cc3-420d-928b-b0a3d65a4611'}) response = self.client.post(url_promote, event_create_data, HTTP_AUTHORIZATION=auth, format='json') - # print(response.json()) - - self.first_ad_view_reward_plan.refresh_from_db() - self.assertEqual(self.first_ad_view_reward_plan.balance, 10000 - 800) - - # url_get_promotion = reverse('promotions-application:plan-list', - # kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": self.user_1_uuid}) - url_get_promotion = reverse('promotions-application:application-user-plan-detail', - kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": '1f9c72e4-3cc3-420d-928b-b0a3d65a4612'}) - - response = self.client.get(url_get_promotion, HTTP_AUTHORIZATION=auth, format='json') print(response.json()) + + def test_application_user__user_not_fount__404(self): + self.first_ad_view_reward_plan, _created = Plan.objects.get_or_create( + title=self.first_ad_view_reward_label, + balance=10000, + user=self.user_1, + application=self.application, + event_list=[self.first_ad_view_reward_label] + ) + + auth = self._create_authorization_header(self.application_access_token.token) + event_create_data = {} + + url_promote = reverse('promotions-application:application-user-plan-promote', + kwargs={"pk": self.first_ad_view_reward_plan.uuid, + "user_uuid": '11111111-1111-1111-1111-111111111111'}) + response = self.client.post(url_promote, event_create_data, HTTP_AUTHORIZATION=auth, format='json') + print(response.json()) + self.assertEqual(response.status_code, 404) diff --git a/apps/promotions/views_application.py b/apps/promotions/views_application.py index becd075..a75eb65 100644 --- a/apps/promotions/views_application.py +++ b/apps/promotions/views_application.py @@ -36,8 +36,6 @@ class ApplicationUserPlanViewSet( def get_queryset(self): application = get_application(self.request) - # TODO: get user from accounts - if not self.request.user: user_uuid = self.kwargs.get('user_uuid') user = User.objects.filter(uuid=user_uuid).first()