Compare commits

...

10 Commits

Author SHA1 Message Date
behruz-dev
f5b7600e2b change field 2025-09-27 17:59:20 +05:00
behruz-dev
abc6a00fe8 add: add full_text fields 2025-09-27 14:41:24 +05:00
behruz-dev
c47c33a1fe change docker-compose.yaml file 2025-09-21 10:06:58 +05:00
behruz-dev
b8b68d0dbb fix 2025-09-18 15:04:53 +05:00
behruz-dev
0252033586 add: add payme api 2025-09-16 21:30:23 +05:00
behruz-dev
795eb7a5a9 fix 2025-09-16 17:04:04 +05:00
behruz-dev
f5a4f879be fix 2025-09-16 16:59:17 +05:00
behruz-dev
260333cea9 change: change response 2025-09-15 15:21:56 +05:00
behruz-dev
787dd0a50a fix 2025-09-15 15:20:07 +05:00
behruz-dev
02bcae05a3 fix proxy 2025-09-13 15:28:17 +05:00
16 changed files with 321 additions and 74 deletions

View File

@@ -14,4 +14,10 @@ EMAIL_HOST_PASSWORD=
EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST='smtp.gmail.com' EMAIL_HOST='smtp.gmail.com'
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_USE_TLS=True EMAIL_USE_TLS=True
CONSUMER_KEY=
CONSUMER_SECRET=
STORE_ID=
API_KEY=

View File

@@ -25,6 +25,8 @@ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'rest_framework_simplejwt', 'rest_framework_simplejwt',
'corsheaders', 'corsheaders',
'payme',
"ckeditor",
# apps # apps
'core.apps.accounts', 'core.apps.accounts',
'core.apps.orders', 'core.apps.orders',
@@ -149,6 +151,20 @@ CONSUMER_SECRET = env.str('CONSUMER_SECRET')
STORE_ID = env.str('STORE_ID') STORE_ID = env.str('STORE_ID')
API_KEY = env.str('API_KEY') API_KEY = env.str('API_KEY')
GLOBAL_CONSUMER_KEY = env.str('GLOBAL_CONSUMER_KEY')
GLOBAL_CONSUMER_SECRET = env.str('GLOBAL_CONSUMER_SECRET') PAYME_ID = env.str('PAYME_ID')
GLOBAL_STORE_ID = env.str('GLOBAL_STORE_ID') PAYME_KEY = env.str('PAYME_KEY')
PAYME_ACCOUNT_FIELD = "order_number"
PAYME_AMOUNT_FIELD = "total_price"
PAYME_ACCOUNT_MODEL = "core.apps.orders.models.Order"
PAYME_ONE_TIME_PAYMENT = True
PAYME_ACCOUNT_FIELD_TYPE = "int"
CKEDITOR_CONFIGS = {
"default": {
"toolbar": "full",
"height": 300,
"width": "100%",
}
}

View File

@@ -8,6 +8,8 @@ from rest_framework.permissions import IsAdminUser
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
from drf_yasg import openapi from drf_yasg import openapi
from core.apps.payment.views import PaymeCallBackAPIView
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( openapi.Info(
@@ -34,7 +36,8 @@ urlpatterns = [
path('orders/', include('core.apps.orders.urls')), path('orders/', include('core.apps.orders.urls')),
path('payment/', include('core.apps.payment.urls')), path('payment/', include('core.apps.payment.urls')),
] ]
)) )),
path('payment/update/', PaymeCallBackAPIView.as_view()),
] ]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2 on 2025-09-27 14:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0007_userterms_text_en_userterms_text_ru_and_more'),
]
operations = [
migrations.AddField(
model_name='aboutus',
name='full_text',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='news',
name='full_text',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='service',
name='full_text',
field=models.TextField(null=True),
),
]

View File

@@ -0,0 +1,58 @@
# Generated by Django 5.2 on 2025-09-27 14:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0008_aboutus_full_text_news_full_text_service_full_text'),
]
operations = [
migrations.AddField(
model_name='aboutus',
name='full_text_en',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='aboutus',
name='full_text_ru',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='aboutus',
name='full_text_uz',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='news',
name='full_text_en',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='news',
name='full_text_ru',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='news',
name='full_text_uz',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='service',
name='full_text_en',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='service',
name='full_text_ru',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='service',
name='full_text_uz',
field=models.TextField(null=True),
),
]

View File

@@ -0,0 +1,74 @@
# Generated by Django 5.2 on 2025-09-27 17:57
import ckeditor.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('common', '0009_aboutus_full_text_en_aboutus_full_text_ru_and_more'),
]
operations = [
migrations.AlterField(
model_name='aboutus',
name='full_text',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='aboutus',
name='full_text_en',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='aboutus',
name='full_text_ru',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='aboutus',
name='full_text_uz',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='news',
name='full_text',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='news',
name='full_text_en',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='news',
name='full_text_ru',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='news',
name='full_text_uz',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='service',
name='full_text',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='service',
name='full_text_en',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='service',
name='full_text_ru',
field=ckeditor.fields.RichTextField(null=True),
),
migrations.AlterField(
model_name='service',
name='full_text_uz',
field=ckeditor.fields.RichTextField(null=True),
),
]

View File

@@ -1,6 +1,8 @@
import uuid import uuid
from django.db import models from django.db import models
from ckeditor.fields import RichTextField
class BaseModel(models.Model): class BaseModel(models.Model):
id = models.UUIDField(primary_key=True, editable=False, unique=True, db_index=True, default=uuid.uuid4) id = models.UUIDField(primary_key=True, editable=False, unique=True, db_index=True, default=uuid.uuid4)
@@ -32,6 +34,7 @@ class Banner(BaseModel):
class AboutUs(BaseModel): class AboutUs(BaseModel):
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
description = models.TextField() description = models.TextField()
full_text = RichTextField(null=True)
def __str__(self): def __str__(self):
return self.title return self.title
@@ -67,6 +70,7 @@ class Service(BaseModel):
text = models.TextField() text = models.TextField()
icon = models.ImageField(upload_to='service/icons/') icon = models.ImageField(upload_to='service/icons/')
image = models.ImageField(upload_to='service/images/') image = models.ImageField(upload_to='service/images/')
full_text = RichTextField(null=True)
def __str__(self): def __str__(self):
return self.title return self.title
@@ -104,6 +108,7 @@ class News(BaseModel):
image = models.ImageField(unique='news/images/') image = models.ImageField(unique='news/images/')
title = models.CharField(max_length=300) title = models.CharField(max_length=300)
text = models.TextField() text = models.TextField()
full_text = RichTextField(null=True)
def __str__(self): def __str__(self):
return self.title return self.title

View File

@@ -34,7 +34,7 @@ class AboutUsSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.AboutUs model = models.AboutUs
fields = [ fields = [
'id', 'title', 'description', 'images', 'features' 'id', 'title', 'description', 'full_text', 'images', 'features'
] ]
@@ -50,14 +50,14 @@ class ServiceListSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Service model = models.Service
fields = [ fields = [
'id', 'title', 'text', 'icon', 'image', 'id', 'title', 'text', 'icon', 'image', 'full_text'
] ]
class NewsSerializer(serializers.ModelSerializer): class NewsSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.News model = models.News
fields = ['id', 'image', 'title', 'text'] fields = ['id', 'image', 'title', 'text', 'full_text']
class ContactUsSerializer(serializers.ModelSerializer): class ContactUsSerializer(serializers.ModelSerializer):

View File

@@ -13,7 +13,7 @@ class BannerTranslation(translator.TranslationOptions):
@translator.register(models.AboutUs) @translator.register(models.AboutUs)
class AboutUsTranslation(translator.TranslationOptions): class AboutUsTranslation(translator.TranslationOptions):
fields = [ fields = [
'title', 'description', 'title', 'description', 'full_text'
] ]
@@ -27,14 +27,14 @@ class AboutUsFeatureTranslation(translator.TranslationOptions):
@translator.register(models.Service) @translator.register(models.Service)
class ServiceTranslation(translator.TranslationOptions): class ServiceTranslation(translator.TranslationOptions):
fields = [ fields = [
'title', 'text', 'title', 'text', 'full_text'
] ]
@translator.register(models.News) @translator.register(models.News)
class NewsTranslation(translator.TranslationOptions): class NewsTranslation(translator.TranslationOptions):
fields = [ fields = [
'title', 'text' 'title', 'text', 'full_text'
] ]

View File

@@ -2,7 +2,6 @@ from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from core.apps.common import models, serializers from core.apps.common import models, serializers
from core.apps.payment.views import get_client_ip
class SiteConfigApiView(generics.GenericAPIView): class SiteConfigApiView(generics.GenericAPIView):

View File

@@ -20,4 +20,13 @@ class VisaPaymentSerializer(serializers.Serializer):
def validate_order_number(self, value): def validate_order_number(self, value):
if not Order.objects.filter(order_number=value).exists(): if not Order.objects.filter(order_number=value).exists():
raise serializers.ValidationError("Order not found") raise serializers.ValidationError("Order not found")
return value return value
class PaymeSerializer(serializers.Serializer):
order_id = serializers.UUIDField()
def validate_order_id(self, value):
if not Order.objects.filter(id=value).exists():
raise serializers.ValidationError("order not found")
return value

View File

@@ -1,9 +1,10 @@
from django.urls import path from django.urls import path
from .views import AtmosCallbackApiView, PaymentGenerateLinkApiView, VisaMastercardPaymentApiView from .views import AtmosCallbackApiView, PaymentGenerateLinkApiView, VisaMastercardPaymentApiView, PayPaymeApiView
urlpatterns = [ urlpatterns = [
path('callback/', AtmosCallbackApiView.as_view()), path('callback/', AtmosCallbackApiView.as_view()),
path('payment/', PaymentGenerateLinkApiView.as_view()), path('payment/', PaymentGenerateLinkApiView.as_view()),
path('visa_mastercard/payment/', VisaMastercardPaymentApiView.as_view()), path('visa_mastercard/payment/', VisaMastercardPaymentApiView.as_view()),
path('payme/', PayPaymeApiView.as_view()),
] ]

View File

@@ -1,6 +1,9 @@
import hashlib import hashlib
import uuid import uuid
from payme.views import PaymeWebHookAPIView, PaymeTransactions
from payme import Payme
from django.conf import settings from django.conf import settings
from rest_framework.generics import GenericAPIView from rest_framework.generics import GenericAPIView
@@ -9,17 +12,18 @@ from rest_framework.response import Response
from rest_framework import status, permissions from rest_framework import status, permissions
from core.apps.orders.models import Order from core.apps.orders.models import Order
from core.apps.payment.serializers import PaymentSerializer, VisaPaymentSerializer from core.apps.payment.serializers import PaymentSerializer, VisaPaymentSerializer, PaymeSerializer
from core.services.payment import Atmos from core.services.payment import Atmos
payme = Payme(settings.PAYME_ID)
def get_client_ip(request): # def get_client_ip(request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") # x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for: # if x_forwarded_for:
ip = x_forwarded_for.split(",")[0] # ip = x_forwarded_for.split(",")[0]
else: # else:
ip = request.META.get("REMOTE_ADDR") # ip = request.META.get("REMOTE_ADDR")
return ip # return ip
class AtmosCallbackApiView(APIView): class AtmosCallbackApiView(APIView):
@@ -27,7 +31,7 @@ class AtmosCallbackApiView(APIView):
permission_classes = [] permission_classes = []
def post(self, request): def post(self, request):
client_ip = get_client_ip(request) # client_ip = get_client_ip(request)
# if client_ip not in settings.ALLOWED_ATMOS_IPS: # if client_ip not in settings.ALLOWED_ATMOS_IPS:
# return Response({"status": 0, "message": "IP ruxsat etilmagan"}, status=403) # return Response({"status": 0, "message": "IP ruxsat etilmagan"}, status=403)
data = request.data data = request.data
@@ -104,4 +108,58 @@ class VisaMastercardPaymentApiView(GenericAPIView):
request_id=str(uuid.uuid4()), request_id=str(uuid.uuid4()),
amount=data.get('amount'), amount=data.get('amount'),
) )
return Response(res) return Response({'success': True, 'link': res})
class PaymeCallBackAPIView(PaymeWebHookAPIView):
def handle_created_payment(self, params, result, *args, **kwargs):
"""
Handle the successful payment. You can override this method
"""
print(f"Transaction created for this params: {params} and cr_result: {result}")
def handle_successfully_payment(self, params, result, *args, **kwargs):
"""
Handle the successful payment. You can override this method
"""
transaction = PaymeTransactions.get_by_transaction_id(
transaction_id=params['id']
)
order = Order.objects.get(id=transaction.id)
order.is_paid = True
order.save()
print(f"Transaction successfully performed for this params: {params} and performed_result: {result}")
def handle_cancelled_payment(self, params, result, *args, **kwargs):
"""
Handle the cancelled payment. You can override this method
"""
transaction = PaymeTransactions.get_by_transaction_id(
transaction_id=params['id']
)
if transaction.state == PaymeTransactions.CANCELED:
order = Order.objects.get(id=transaction.id)
order.is_paid = False
order.save()
print(f"Transaction cancelled for this params: {params} and cancelled_result: {result}")
class PayPaymeApiView(GenericAPIView):
serializer_class = PaymeSerializer
permission_classes = [permissions.IsAuthenticated]
queryset = Order.objects.all()
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
order_id = serializer.validated_data.get('order_id')
order = Order.objects.get(id=order_id)
payment_link = payme.initializer.generate_pay_link(
id=order_id,
amount=order.total_price * 100,
return_url="https://wisdom.uz",
)
return Response({'success': True, 'link': payment_link}, status=200)

View File

@@ -9,9 +9,6 @@ class Atmos:
self.consumer_secret = settings.CONSUMER_SECRET self.consumer_secret = settings.CONSUMER_SECRET
self.terminal_id = terminal_id self.terminal_id = terminal_id
self.store_id = settings.STORE_ID self.store_id = settings.STORE_ID
self.global_consumer_key = settings.GLOBAL_CONSUMER_KEY
self.global_consumer_secret = settings.GLOBAL_CONSUMER_SECRET
self.global_store_id = settings.GLOBAL_STORE_ID
def login(self): def login(self):
credentials = f"{self.consumer_key}:{self.consumer_secret}" credentials = f"{self.consumer_key}:{self.consumer_secret}"
@@ -22,44 +19,6 @@ class Atmos:
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
} }
data = {
"grant_type": "client_credentials"
}
url = 'https://partner.atmos.uz/token'
res = requests.post(url, headers=headers, data=data)
return res.json()['access_token']
def create_transaction(self, amount, account):
access_token = self.login()
url = 'https://partner.atmos.uz/merchant/pay/create'
headers = {
'Authorization': f'Bearer {access_token}',
}
data = {
'amount': int(amount) * 100,
'account': str(account),
'store_id': f'{self.store_id}'
}
res = requests.post(url, headers=headers, json=data)
return res.json()
def generate_url(self, transaction_id, redirect_url):
url = f'http://test-checkout.pays.uz/invoice/get?storeId={self.store_id}&transactionId={transaction_id}&redirectLink={redirect_url}'
return url
# Visa/MasterCard
def login_global_payment(self):
credentials = f"{self.global_consumer_key}:{self.global_consumer_secret}"
encoded_credentials = base64.b64encode(credentials.encode()).decode()
headers = {
"Authorization": f"Basic {encoded_credentials}",
"Content-Type": "application/x-www-form-urlencoded",
}
data = { data = {
"grant_type": "client_credentials" "grant_type": "client_credentials"
} }
@@ -67,8 +26,32 @@ class Atmos:
res = requests.post(url, headers=headers, data=data) res = requests.post(url, headers=headers, data=data)
return res.json()['access_token'] return res.json()['access_token']
def create_transaction(self, amount, account):
access_token = self.login()
url = 'https://apigw.atmos.uz/merchant/pay/create'
headers = {
'Authorization': f'Bearer {access_token}',
}
data = {
'amount': int(amount) * 100,
'account': str(account),
'store_id': f'{self.store_id}'
}
res = requests.post(url, headers=headers, json=data)
return res.json()
def generate_url(self, transaction_id, redirect_url):
# url = f'https://checkout.pays.uz/invoice/get?storeId={self.store_id}&transactionId={transaction_id}&redirectLink={redirect_url}'
url = f'http://test-checkout.pays.uz/invoice/get?storeId={self.store_id}&transactionId={transaction_id}&redirectLink={redirect_url}'
return url
# Visa/MasterCard
def global_payment(self, request_id, account, amount): def global_payment(self, request_id, account, amount):
access = self.login_global_payment() access = self.login()
url = 'https://apigw.atmos.uz/checkout/invoice/create' url = 'https://apigw.atmos.uz/checkout/invoice/create'
headers = { headers = {
'Authorization': f'Bearer {access}', 'Authorization': f'Bearer {access}',
@@ -76,17 +59,14 @@ class Atmos:
} }
data = { data = {
"request_id": request_id, "request_id": request_id,
"store_id": self.global_store_id, "store_id": self.store_id,
"account": str(account), "account": str(account),
"amount": amount * 100, "amount": amount * 100,
"success_url": "https://wisdom.uz", "success_url": "https://wisdom.uz",
} }
res = requests.post(url=url, headers=headers, json=data, proxies={ res = requests.post(url=url, headers=headers, json=data)
'http': 'http://94.230.232.47:8080',
'https': 'http://94.230.232.47:8080',
})
if res.status_code == 200: if res.status_code == 200:
return res.json() return res.json()['url']
else: else:
return res.json() return res.json()

View File

@@ -20,6 +20,8 @@ services:
dockerfile: ./docker/Dockerfile.nginx dockerfile: ./docker/Dockerfile.nginx
depends_on: depends_on:
- web - web
restart: always
web: web:
networks: networks:
@@ -39,6 +41,7 @@ services:
- 8.8.8.8 - 8.8.8.8
- 8.8.4.4 - 8.8.4.4
- 1.1.1.1 - 1.1.1.1
restart: always
db: db:
image: postgres:17 image: postgres:17
@@ -50,6 +53,8 @@ services:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
volumes: volumes:
- pd_data:/var/lib/postgresql/data - pd_data:/var/lib/postgresql/data
restart: always
redis: redis:
networks: networks:
@@ -57,6 +62,8 @@ services:
image: redis image: redis
ports: ports:
- "6380:6379" - "6380:6379"
restart: always
celery: celery:
networks: networks:
@@ -69,4 +76,5 @@ services:
- redis - redis
- web - web
volumes: volumes:
- "./:/code" - "./:/code"
restart: always

View File

@@ -37,4 +37,6 @@ uritemplate==4.2.0
uvicorn==0.35.0 uvicorn==0.35.0
vine==5.1.0 vine==5.1.0
wcwidth==0.2.13 wcwidth==0.2.13
requests requests
payme-pkg
django-ckeditor