contract signature part is done

This commit is contained in:
behruz-dev
2025-07-17 14:49:06 +05:00
parent 4bde93f3ed
commit 7102cdbcfd
18 changed files with 209 additions and 26 deletions

View File

@@ -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,

View File

@@ -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)
)

View File

@@ -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,

View File

@@ -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)
# send_sms_eskiz(user.phone, code)
send_sms_code(code, 'auth', user.phone)

View File

@@ -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']
list_display = ['id', 'contract_side', 'contract', 'status']
@admin.register(ContractSignatureCode)
class ContractSignatureCodeAdmin(admin.ModelAdmin):
list_display = ['id', 'code', 'signature']

View File

@@ -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'),
)

View File

@@ -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',
},
),
]

View File

@@ -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'

View File

@@ -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',
]

View File

@@ -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'
]
'id', 'full_name', 'user', 'contract_signature'
]
def get_contract_signature(self, obj):
contract_signature = obj.contract_signatures
return ContractSignatureListSerializer(contract_signature).data

View File

@@ -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

View File

@@ -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,
)
contract_side=contract_side,
)

View File

@@ -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)

View File

@@ -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/<uuid:signature_id>/', contract_signature_views.SendContractSignatureCodeApiView.as_view(), name='send-signature-code'),
path('sign_contract/', contract_signature_views.SigningContractApiView.as_view(), name='sign-contract'),
]
))
]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -14,4 +14,4 @@ pytest
pytest-django
django-cacheops==7.2
django-unfold==0.62.0
drf_yasg==1.21.10
drf_yasg==1.21.10