From 7102cdbcfdf8948202c4f952b497d136f0dcebda Mon Sep 17 00:00:00 2001 From: behruz-dev Date: Thu, 17 Jul 2025 14:49:06 +0500 Subject: [PATCH] contract signature part is done --- config/conf/simplejwt.py | 2 +- config/env.py | 3 +- core/apps/accounts/models/user.py | 2 +- core/apps/accounts/tasks/user.py | 4 +- core/apps/contracts/admins/contract.py | 10 ++++- core/apps/contracts/enums/contract.py | 3 +- .../apps/contracts/migrations/0001_initial.py | 27 +++++++++--- core/apps/contracts/models/contract.py | 37 +++++++++++++--- core/apps/contracts/serializers/contract.py | 2 +- .../contracts/serializers/contract_side.py | 13 ++++-- .../serializers/contract_signature.py | 34 ++++++++++++++ core/apps/contracts/tasks/contract_side.py | 10 +++-- .../contracts/tasks/contract_signature.py | 14 ++++++ core/apps/contracts/urls.py | 7 +++ .../contracts/views/contract_signature.py | 44 +++++++++++++++++++ core/services/sms_via_bot.py | 13 ++++++ docker-compose.yaml | 8 +++- requirements.txt | 2 +- 18 files changed, 209 insertions(+), 26 deletions(-) create mode 100644 core/apps/contracts/serializers/contract_signature.py create mode 100644 core/apps/contracts/tasks/contract_signature.py create mode 100644 core/apps/contracts/views/contract_signature.py create mode 100644 core/services/sms_via_bot.py diff --git a/config/conf/simplejwt.py b/config/conf/simplejwt.py index a02ea80..e360737 100644 --- a/config/conf/simplejwt.py +++ b/config/conf/simplejwt.py @@ -3,7 +3,7 @@ from datetime import timedelta from config.env import env SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "ACCESS_TOKEN_LIFETIME": timedelta(days=1), "REFRESH_TOKEN_LIFETIME": timedelta(days=30), "ROTATE_REFRESH_TOKENS": False, "BLACKLIST_AFTER_ROTATION": False, diff --git a/config/env.py b/config/env.py index bd861c9..4d4259c 100644 --- a/config/env.py +++ b/config/env.py @@ -14,5 +14,6 @@ env = environ.Env( DB_PORT=(int, 5432), DEBUG=(bool, False), ALLOWED_HOSTS=(list, ['localhost', '127.0.0.1']), - SECRET_KEY=(str) + SECRET_KEY=(str), + BOT_TOKEN=(str) ) \ No newline at end of file diff --git a/core/apps/accounts/models/user.py b/core/apps/accounts/models/user.py index 52ea28c..e3fb32f 100644 --- a/core/apps/accounts/models/user.py +++ b/core/apps/accounts/models/user.py @@ -26,7 +26,7 @@ class User(BaseModel, AbstractUser): return self.phone def generate_code(self): - code = ''.join([str(random.randint(0, 100) % 10) for _ in range(4)]) + code = ''.join([str(random.randint(1, 100) % 10) for _ in range(4)]) expiration_time = timezone.now() + datetime.timedelta(minutes=2) VerificationCode.objects.create( code=code, diff --git a/core/apps/accounts/tasks/user.py b/core/apps/accounts/tasks/user.py index 0d3d8c7..67c9516 100644 --- a/core/apps/accounts/tasks/user.py +++ b/core/apps/accounts/tasks/user.py @@ -3,9 +3,11 @@ from celery import shared_task from core.apps.accounts.models.verification_code import VerificationCode from core.apps.accounts.models.user import User from core.services.sms import send_sms_eskiz +from core.services.sms_via_bot import send_sms_code @shared_task def create_and_send_sms_code(user): user = User.objects.get(id=user) code = user.generate_code() - send_sms_eskiz(user.phone, code) \ No newline at end of file + # send_sms_eskiz(user.phone, code) + send_sms_code(code, 'auth', user.phone) diff --git a/core/apps/contracts/admins/contract.py b/core/apps/contracts/admins/contract.py index e2ce0cb..ba8c076 100644 --- a/core/apps/contracts/admins/contract.py +++ b/core/apps/contracts/admins/contract.py @@ -1,12 +1,13 @@ from django.contrib import admin -from core.apps.contracts.models.contract import Contract, ContractSide, ContractSignature +from core.apps.contracts.models.contract import Contract, ContractSide, ContractSignature, ContractSignatureCode @admin.register(Contract) class ContractAdmin(admin.ModelAdmin): list_display = ['id', 'contract_number', 'name', 'face_id', 'attach_file', 'add_folder', 'add_notification'] + @admin.register(ContractSide) class ContractSideAdmin(admin.ModelAdmin): list_display = ['id', 'full_name'] @@ -14,4 +15,9 @@ class ContractSideAdmin(admin.ModelAdmin): @admin.register(ContractSignature) class ContractSignatureAdmin(admin.ModelAdmin): - list_display = ['id', 'user', 'contract', 'status'] \ No newline at end of file + list_display = ['id', 'contract_side', 'contract', 'status'] + + +@admin.register(ContractSignatureCode) +class ContractSignatureCodeAdmin(admin.ModelAdmin): + list_display = ['id', 'code', 'signature'] \ No newline at end of file diff --git a/core/apps/contracts/enums/contract.py b/core/apps/contracts/enums/contract.py index 8001e35..71e5515 100644 --- a/core/apps/contracts/enums/contract.py +++ b/core/apps/contracts/enums/contract.py @@ -8,5 +8,6 @@ STATUS = ( ('created', 'created',), ('signed_company', 'signed by company',), ('signed_customer', 'signed by customer',), - ('cancelled', 'cancelled') + ('contract_signed', 'contract signed'), + ('cancelled', 'cancelled'), ) \ No newline at end of file diff --git a/core/apps/contracts/migrations/0001_initial.py b/core/apps/contracts/migrations/0001_initial.py index d2d6b68..47ef3ba 100644 --- a/core/apps/contracts/migrations/0001_initial.py +++ b/core/apps/contracts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-07-16 17:14 +# Generated by Django 5.2 on 2025-07-17 14:48 import django.db.models.deletion import uuid @@ -25,7 +25,7 @@ class Migration(migrations.Migration): ('contract_number', models.PositiveIntegerField()), ('name', models.CharField(max_length=200)), ('sides', models.CharField(choices=[('two_or_more', 'two or more'), ('customer_only', 'customer only'), ('only_company', 'onlycompany')], max_length=13)), - ('status', models.CharField(choices=[('created', 'created'), ('signed_company', 'signed by company'), ('signed_customer', 'signed by customer'), ('cancelled', 'cancelled')], default='created', max_length=15)), + ('status', models.CharField(choices=[('created', 'created'), ('signed_company', 'signed by company'), ('signed_customer', 'signed by customer'), ('contract_signed', 'contract signed'), ('cancelled', 'cancelled')], default='created', max_length=15)), ('face_id', models.BooleanField(default=False)), ('attach_file', models.BooleanField(default=False)), ('add_folder', models.BooleanField(default=False)), @@ -69,12 +69,29 @@ class Migration(migrations.Migration): ('signature_type', models.CharField(blank=True, choices=[('sms_sign', 'sms signature'), ('electronic_sing', 'electronic signature')], max_length=20, null=True)), ('is_signature', models.BooleanField(default=False)), ('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contract_signatures', to='contracts.contract')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contract_users', to=settings.AUTH_USER_MODEL)), + ('contract_side', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='contract_signatures', to='contracts.contractside')), ], options={ - 'verbose_name': 'contract signatures', + 'verbose_name': 'contract signature', + 'verbose_name_plural': 'contract signatures', 'db_table': 'contract_signatures', - 'unique_together': {('contract', 'user')}, + 'unique_together': {('contract', 'contract_side')}, + }, + ), + migrations.CreateModel( + name='ContractSignatureCode', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('code', models.PositiveSmallIntegerField()), + ('expiration_time', models.DateTimeField()), + ('signature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='signature_codes', to='contracts.contractsignature')), + ], + options={ + 'verbose_name': 'contract signature code', + 'verbose_name_plural': 'contract signature codes', + 'db_table': 'contract_signature_codes', }, ), ] diff --git a/core/apps/contracts/models/contract.py b/core/apps/contracts/models/contract.py index 90620d4..d101b0b 100644 --- a/core/apps/contracts/models/contract.py +++ b/core/apps/contracts/models/contract.py @@ -1,6 +1,9 @@ +import random +from datetime import timedelta + from django.db import models from django.contrib.auth import get_user_model -from django.contrib.postgres.fields import ArrayField +from django.utils import timezone from core.apps.shared.models.base import BaseModel from core.apps.contracts.enums.contract import SIDES, STATUS @@ -56,7 +59,7 @@ class ContractSide(BaseModel): class ContractSignature(BaseModel): contract = models.ForeignKey(Contract, on_delete=models.CASCADE, related_name='contract_signatures') - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='contract_users') + contract_side = models.OneToOneField(ContractSide, on_delete=models.CASCADE, related_name='contract_signatures') status = models.CharField(max_length=20, choices=SIGNATURE_STATUS, default='organized') signature_type = models.CharField(max_length=20, choices=SIGNATURE_TYPE, null=True, blank=True) @@ -64,10 +67,34 @@ class ContractSignature(BaseModel): is_signature = models.BooleanField(default=False) def __str__(self): - return f'{self.user} user signature for {self.contract} contract' + return f'{self.contract_side} user signature for {self.contract} contract' + + def generate_code(self): + code = ''.join([str(random.randint(1, 9) % 10) for _ in range(4)]) + ContractSignatureCode.objects.create( + code=code, + signature=self, + expiration_time = timezone.now() + timedelta(minutes=2) + ) + return code class Meta: verbose_name = 'contract signature' - verbose_name = 'contract signatures' + verbose_name_plural = 'contract signatures' db_table = 'contract_signatures' - unique_together = ['contract', 'user'] + unique_together = ['contract', 'contract_side'] + + +class ContractSignatureCode(BaseModel): + code = models.PositiveSmallIntegerField() + signature = models.ForeignKey(ContractSignature, on_delete=models.CASCADE, related_name='signature_codes') + expiration_time = models.DateTimeField() + + def __str__(self): + return f'{self.code} - {self.signature}' + + class Meta: + verbose_name = 'contract signature code' + verbose_name_plural = 'contract signature codes' + db_table = 'contract_signature_codes' + \ No newline at end of file diff --git a/core/apps/contracts/serializers/contract.py b/core/apps/contracts/serializers/contract.py index de99a01..10e30ba 100644 --- a/core/apps/contracts/serializers/contract.py +++ b/core/apps/contracts/serializers/contract.py @@ -47,5 +47,5 @@ class ContractDetailSerializer(serializers.ModelSerializer): class Meta: model = Contract fields = [ - 'id', 'name', 'file', 'contract_number', 'contract_sides', + 'id', 'name', 'file', 'status', 'contract_number', 'contract_sides', ] \ No newline at end of file diff --git a/core/apps/contracts/serializers/contract_side.py b/core/apps/contracts/serializers/contract_side.py index 2aba5b2..f45a8b7 100644 --- a/core/apps/contracts/serializers/contract_side.py +++ b/core/apps/contracts/serializers/contract_side.py @@ -2,8 +2,9 @@ from django.contrib.auth import get_user_model from rest_framework import serializers -from core.apps.contracts.models.contract import ContractSide, Contract +from core.apps.contracts.models.contract import ContractSide, Contract, ContractSignature from core.apps.contracts.enums.contract_side import ROLE +from core.apps.contracts.serializers.contract_signature import ContractSignatureListSerializer User = get_user_model() @@ -25,8 +26,14 @@ class ContractSideCreateSerializer(serializers.Serializer): class ContractSideListSerializer(serializers.ModelSerializer): + contract_signature = serializers.SerializerMethodField(method_name='get_contract_signature') + class Meta: model = ContractSide fields = [ - 'id', 'full_name', 'user' - ] \ No newline at end of file + 'id', 'full_name', 'user', 'contract_signature' + ] + + def get_contract_signature(self, obj): + contract_signature = obj.contract_signatures + return ContractSignatureListSerializer(contract_signature).data \ No newline at end of file diff --git a/core/apps/contracts/serializers/contract_signature.py b/core/apps/contracts/serializers/contract_signature.py new file mode 100644 index 0000000..315e81b --- /dev/null +++ b/core/apps/contracts/serializers/contract_signature.py @@ -0,0 +1,34 @@ +from django.utils import timezone + +from rest_framework import serializers + +from core.apps.contracts.models.contract import ContractSignature, ContractSignatureCode + + +class ContractSignatureListSerializer(serializers.ModelSerializer): + class Meta: + model = ContractSignature + fields = [ + 'id', 'status', 'signature_type', 'is_signature' + ] + + +class ContractSignatureSerializer(serializers.Serializer): + code = serializers.IntegerField() + signature_id = serializers.UUIDField() + + def validate(self, data): + user = self.context.get('user') + signature = ContractSignature.objects.filter(id=data.get('signature_id')).first() + if not signature: + raise serializers.ValidationError({"detail": "contract signature not found"}) + if signature.contract_side.user != user: + raise serializers.ValidationError({'detail': 'this is not your code'}) + signature_code = ContractSignatureCode.objects.filter(signature=signature, code=data.get('code')).first() + if not signature_code: + raise serializers.ValidationError({'detail': 'invalid code'}) + if signature_code.expiration_time < timezone.now(): + raise serializers.ValidationError({"detail": 'code is expired'}) + data['contract'] = signature.contract_side.contract + data['contract_signature'] = signature + return data diff --git a/core/apps/contracts/tasks/contract_side.py b/core/apps/contracts/tasks/contract_side.py index dd5ea21..3c882df 100644 --- a/core/apps/contracts/tasks/contract_side.py +++ b/core/apps/contracts/tasks/contract_side.py @@ -3,6 +3,8 @@ from django.contrib.auth import get_user_model from celery import shared_task from core.apps.contracts.models.contract import ContractSide, Contract, ContractSignature +from core.services.sms_via_bot import send_sms_code + @shared_task def create_contract_side(data): @@ -11,7 +13,7 @@ def create_contract_side(data): contract = Contract.objects.get(id=data['contract_id']) user = User.objects.get(phone=data['phone']) - ContractSide.objects.create( + contract_side = ContractSide.objects.create( full_name=data.get('full_name'), indentification=data.get('indentification'), position=data.get('position'), @@ -23,5 +25,7 @@ def create_contract_side(data): ContractSignature.objects.create( contract=contract, - user=user, - ) \ No newline at end of file + contract_side=contract_side, + ) + + diff --git a/core/apps/contracts/tasks/contract_signature.py b/core/apps/contracts/tasks/contract_signature.py new file mode 100644 index 0000000..a0fc676 --- /dev/null +++ b/core/apps/contracts/tasks/contract_signature.py @@ -0,0 +1,14 @@ +from django.shortcuts import get_object_or_404 + +from celery import shared_task + +from core.apps.contracts.models.contract import ContractSignature, ContractSignatureCode +from core.services.sms_via_bot import send_sms_code + + +@shared_task +def send_contract_signature_code(signature_id): + contract_signature = get_object_or_404(ContractSignature, id=signature_id) + code = contract_signature.generate_code() + send_sms_code(code, 'contract', contract_signature.contract_side.user.phone) + \ No newline at end of file diff --git a/core/apps/contracts/urls.py b/core/apps/contracts/urls.py index 1c3e4ad..755fd85 100644 --- a/core/apps/contracts/urls.py +++ b/core/apps/contracts/urls.py @@ -2,6 +2,7 @@ from django.urls import path, include from core.apps.contracts.views import contract as contract_views from core.apps.contracts.views import contract_side as contract_side_views +from core.apps.contracts.views import contract_signature as contract_signature_views urlpatterns = [ @@ -15,5 +16,11 @@ urlpatterns = [ path('contract_side/', include([ path('create/', contract_side_views.ConstartSideCreateApiView.as_view(), name='contract-side-create'), ] + )), + path('contract_signature/', include( + [ + path('send_signature_code//', contract_signature_views.SendContractSignatureCodeApiView.as_view(), name='send-signature-code'), + path('sign_contract/', contract_signature_views.SigningContractApiView.as_view(), name='sign-contract'), + ] )) ] \ No newline at end of file diff --git a/core/apps/contracts/views/contract_signature.py b/core/apps/contracts/views/contract_signature.py new file mode 100644 index 0000000..7355a7e --- /dev/null +++ b/core/apps/contracts/views/contract_signature.py @@ -0,0 +1,44 @@ +from rest_framework import generics, status, permissions, views +from rest_framework.response import Response + +from core.apps.contracts.models.contract import ContractSignature, ContractSignatureCode +from core.apps.contracts.serializers.contract_signature import ContractSignatureSerializer +from core.apps.contracts.tasks.contract_signature import send_contract_signature_code + + +class SendContractSignatureCodeApiView(views.APIView): + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, signature_id): + # TODO: create and send code with celery in backgroud + send_contract_signature_code.delay(signature_id) + return Response({"success": True, "message": "code send"}, status=status.HTTP_200_OK) + + +class SigningContractApiView(generics.GenericAPIView): + serializer_class = ContractSignatureSerializer + queryset = ContractSignature.objects.all() + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + user = request.user + serializer = self.serializer_class(data=request.data, context={'user': user}) + if serializer.is_valid(): + data = serializer.validated_data + contract = data.get('contract') + contract_signature = data.get('contract_signature') + if contract.company == user: + if contract.status == 'created': + contract.status = 'signed_company' + elif contract.status == 'signed_customer': + contract.status = 'signed_contract' + else: + if contract.status == 'created': + contract.status = 'signed_customer' + elif contract.status == 'signed_company': + contract.status = 'signed_contract' + contract_signature.status = 'signed' + contract_signature.save() + contract.save() + return Response({'success': True, 'message': 'contract is signed'}, status=status.HTTP_200_OK) + return Response({'success': False, 'message': serializer.errors}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/core/services/sms_via_bot.py b/core/services/sms_via_bot.py new file mode 100644 index 0000000..e683d3e --- /dev/null +++ b/core/services/sms_via_bot.py @@ -0,0 +1,13 @@ +import requests + +from config.env import env + +def send_sms_code(code, type, phone): + url = f'https://api.telegram.org/bot{env.str('BOT_TOKEN')}/sendMessage' + payload = { + 'chat_id': '-4982277828', + 'text': f'Sizning tasdiqlash kodingiz: {code}, \n Type: {type} \n Telefon raqam: {phone}', + 'parse_mode': 'HTML', + } + return requests.post(url, data=payload) + \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 579d82d..efbfb6b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -78,4 +78,10 @@ services: ports: - 6379:6379 - + bot: + build: + context: /home/behruz/bots/send-verification-code + dockerfile: Dockerfile + volumes: + - /home/behruz/bots/send-verification-code:/bot + restart: unless-stopped diff --git a/requirements.txt b/requirements.txt index 01b0f15..e917520 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,4 @@ pytest pytest-django django-cacheops==7.2 django-unfold==0.62.0 -drf_yasg==1.21.10 \ No newline at end of file +drf_yasg==1.21.10