Compare commits

..

5 commits

Author SHA1 Message Date
ab348d5134 Merge pull request 'from FEATURE/refactore-application-views into master' (#2) from FEATURE/refactore-application-views into master
Reviewed-on: https://git.mindplus-dev.ir/gooyal/promotions/pulls/2
2026-07-11 06:58:18 -04:00
Sayyid Hamid Mahdavi
a8b10b79a5 user availability test 2026-07-08 12:05:42 +03:30
Sayyid Hamid Mahdavi
1eca11e4a4 better exceptions 2026-07-07 17:24:32 +03:30
Sayyid Hamid Mahdavi
e7db91d1da get user from accounts 2026-07-07 16:42:34 +03:30
Sayyid Hamid Mahdavi
3320ff93e3 temp 2026-07-07 12:10:19 +03:30
7 changed files with 296 additions and 25 deletions

View file

@ -65,7 +65,7 @@ class ApplicationApiFlowsTests(APITestCase):
application_uuid = uuid.UUID(application_uuid_hex) application_uuid = uuid.UUID(application_uuid_hex)
application_access_token = '14naVsdKCbKNOhnElPyXcrwSnqqFbm' application_access_token = '14naVsdKCbKNOhnElPyXcrwSnqqFbm'
user_access_token = '24naVsdKCbKNOhnElPyXcrwSnqqFbm' 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') user_2_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470052')
first_ad_view_reward_label = 'first_ad_view_reward_label' first_ad_view_reward_label = 'first_ad_view_reward_label'
referral_reward_label = 'referral_reward_label' referral_reward_label = 'referral_reward_label'
@ -611,10 +611,111 @@ class ApplicationApiFlowsTests(APITestCase):
self.first_ad_view_reward_plan.refresh_from_db() self.first_ad_view_reward_plan.refresh_from_db()
self.assertEqual(self.first_ad_view_reward_plan.balance, 10000 - 800) 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, # 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') # format='json')
# print(response.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') # 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()) # 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: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()
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:application-user-plan-promote',
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())
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)

View file

@ -54,7 +54,8 @@ class ApplicationRouter(DefaultRouter):
router = DefaultRouter() router = DefaultRouter()
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') router.register('event', views_application.ApplicationEventViewSet, basename='event')
urlpatterns = router.urls urlpatterns = router.urls

View file

@ -2,6 +2,7 @@ from drf_spectacular.utils import extend_schema
from rest_framework import mixins, status from rest_framework import mixins, status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework import exceptions 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.generics import CreateAPIView, get_object_or_404, RetrieveAPIView, ListAPIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -9,20 +10,22 @@ from rest_framework.viewsets import GenericViewSet
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements, TokenMatchesViewSetActions from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements, TokenMatchesViewSetActions
from apps.gooyal_oauth2.utils import get_application from apps.gooyal_oauth2.utils import get_application
from utils.clients.accounts_client import get_user_info
from utils.exceptions import UnprocessableEntity from utils.exceptions import UnprocessableEntity
from .models import Plan, Promotion, EventSaver from .models import Plan, Promotion, EventSaver
from .serializers import PlanSerializer, PromotionSerializer, EventSerializer, PromoteSerializer, UserPlanSerializer from .serializers import PlanSerializer, PromotionSerializer, EventSerializer, PromoteSerializer, UserPlanSerializer
from .tasks import analyze_event_task from .tasks import analyze_event_task
from ..users.models import User from ..users.models import User
from django.utils.translation import gettext as _
class ApplicationPlanViewSet( class ApplicationUserPlanViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
# mixins.CreateModelMixin, # mixins.CreateModelMixin,
GenericViewSet GenericViewSet
): ):
serializer_class = PromotionSerializer serializer_class = UserPlanSerializer
permission_classes = [TokenMatchesViewSetActions] permission_classes = [TokenMatchesViewSetActions]
required_action_scopes = { required_action_scopes = {
@ -33,6 +36,18 @@ class ApplicationPlanViewSet(
def get_queryset(self): def get_queryset(self):
application = get_application(self.request) application = get_application(self.request)
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.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) # return Promotion.objects.filter(application=application)
return Plan.objects.all() return Plan.objects.all()
@ -61,8 +76,8 @@ class ApplicationPlanViewSet(
# TODO # TODO
# try: # try:
user_uuid = self.kwargs.get('user_uuid')
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: # except Exception as e:
# raise exceptions.ValidationError(str(e)) # raise exceptions.ValidationError(str(e))
@ -109,6 +124,24 @@ class ApplicationEventViewSet(
analyze_event_task.delay(event.uuid) analyze_event_task.delay(event.uuid)
serializer.instance = event 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 # NOT
class UserPlanViewSet(mixins.RetrieveModelMixin, class UserPlanViewSet(mixins.RetrieveModelMixin,

View file

@ -382,6 +382,8 @@ CELERY_TASK_TIME_LIMIT = 30 * 60
CELERY_BROKER_URL = REDIS_BASE_URL CELERY_BROKER_URL = REDIS_BASE_URL
CELERY_RESULT_BACKEND = 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_BASE_PUBLIC_URL = config('WALLET_BASE_PUBLIC_URL', default=None, cast=str)
WALLET_RIAL = config('WALLET_RIAL', cast=str) WALLET_RIAL = config('WALLET_RIAL', cast=str)
WALLET_REWARD = config('WALLET_REWARD', cast=str) WALLET_REWARD = config('WALLET_REWARD', cast=str)

View file

@ -0,0 +1,89 @@
import time
import requests
from django.conf import settings
from django.core.cache import cache
from httpx import Request
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('promotions_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.OAUTH2_SCOPES,
}
auth = (settings.OAUTH2_CLIENT_ID, settings.OAUTH2_CLIENT_SECRET)
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('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']
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_info(uuid):
client = get_client()
response: PublicUser = users_api_users_retrieve.sync(client=client, uuid=uuid)
print(response)
return response
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

View file

@ -6,7 +6,8 @@ from django.core.cache import cache
from httpx import Request from httpx import Request
from utils.clients.gooyal_wallet_client import AuthenticatedClient 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, from utils.clients.gooyal_wallet_client.api.wallet import (wallet_api_application_deposit_create,
wallet_api_application_deposit_verify_retrieve, wallet_api_application_deposit_verify_retrieve,
wallet_api_application_withdraw_create, 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) wallet_api_application_user_wallets_list)
def login_as_client_credentials(): 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') # expires_in = self.auth_data.get('expires_in')
# created_at = self.auth_data.get('created_at') # 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/', response = requests.post(f'{settings.OAUTH2_PROVIDER_PUBLIC_URL}/token/',
data=data, data=data,
auth=auth) auth=auth)
print(response.content) print(response.content)
auth_data = response.json() auth_data = response.json()
if 'access_token' in auth_data: if 'access_token' in auth_data:
auth_data['created_at'] = time.time() 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 return auth_data
@ -75,15 +75,16 @@ def deposit_to_user_wallet_submit(payer_wallet, data):
deposit_data = Tmp() deposit_data = Tmp()
client = get_client() client = get_client()
response: ApplicationDeposit = wallet_api_application_deposit_create.sync(payer_wallet=payer_wallet, response: ApplicationDeposit = wallet_api_application_deposit_create.sync(payer_wallet=payer_wallet,
client=client, client=client,
body=deposit_data) body=deposit_data)
print(response) print(response)
return response return response
def deposit_to_user_wallet_verify(payer_wallet, uuid): def deposit_to_user_wallet_verify(payer_wallet, uuid):
client = get_client() 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) print(response)
return response return response
@ -95,14 +96,16 @@ def withdraw_from_user_wallet_submit(payee_wallet, data):
withdraw_data = Tmp() withdraw_data = Tmp()
client = get_client() 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) print(response)
return response return response
def withdraw_from_user_wallet_verify(payee_wallet, uuid): def withdraw_from_user_wallet_verify(payee_wallet, uuid):
client = get_client() 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) print(response)
return response return response
@ -110,7 +113,8 @@ def withdraw_from_user_wallet_verify(payee_wallet, uuid):
def get_user_wallets(owner, wallets): def get_user_wallets(owner, wallets):
client = get_client() client = get_client()
# by pass several wallet query params: [','.join([str(i) for i in wallets])] # 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) print(response)
balances = dict(map(lambda result: (str(result.wallet), result.balance), response.results)) balances = dict(map(lambda result: (str(result.wallet), result.balance), response.results))
return balances return balances

View file

@ -1,5 +1,7 @@
import logging import logging
from django.conf import settings
from django.utils import timezone
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.views import exception_handler as drf_exception_handler 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) response = drf_exception_handler(exc, context)
if response is not None: if response is not None:
# ساختار دلخواه خود را تعریف می‌کنیم
response_data = { response_data = {
'success': False, 'success': False,
'status_code': response.status_code, 'status_code': response.status_code,
'message': 'An error occurred', 'status_message': str(getattr(exc, 'default_detail', exc)),
'details': {} 'details': getattr(exc, 'detail', None),
} }
if isinstance(exc, APIException): if isinstance(exc, APIException):
response_data['message'] = exc.detail if isinstance(exc.detail, str) else "Validation error" try:
response_data['details'] = exc.detail if isinstance(exc.detail, dict) else {} 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 response.data = response_data
@ -47,16 +68,36 @@ class ErrorMiddleware:
{ {
"success": False, "success": False,
"status_code": 500, "status_code": 500,
"message": "Internal server error", "status_message": "Internal server error",
"details": str(exception) # فقط در حالت توسعه! در تولید بهتر است لاگ شود. "details": str(exception) if settings.DEBUG else None,
}, },
status=500 status=500
) )
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework import status
from django.utils.translation import gettext_lazy as _
class UnprocessableEntity(APIException): class UnprocessableEntity(APIException):
status_code = 422 status_code = 422
default_detail = 'The request was well-formed but cannot be processed due to semantic errors.' default_detail = 'The request was well-formed but cannot be processed due to semantic errors.'
default_code = 'unprocessable_entity' 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):