Rename KYC models (Application/Profile) and add public schema with MongoDB
All checks were successful
Build Docker Image / build (push) Successful in 3m7s
All checks were successful
Build Docker Image / build (push) Successful in 3m7s
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
from django.contrib import admin, messages
|
||||
from .models import KYCRequest, KYCMonitoring, KYCRequestRussia
|
||||
from .models import KYCApplication, KYCProfile, KYCDetailsRussia
|
||||
from .temporal import KycWorkflowClient
|
||||
|
||||
|
||||
@admin.action(description="Start KYC workflow")
|
||||
def start_kyc_workflow(modeladmin, request, queryset):
|
||||
"""Start KYC workflow for selected requests."""
|
||||
for kyc_request in queryset:
|
||||
workflow_id = KycWorkflowClient.start(kyc_request)
|
||||
"""Start KYC workflow for selected applications."""
|
||||
for kyc_app in queryset:
|
||||
workflow_id = KycWorkflowClient.start(kyc_app)
|
||||
if workflow_id:
|
||||
messages.success(request, f"Workflow started for {kyc_request.uuid}")
|
||||
messages.success(request, f"Workflow started for {kyc_app.uuid}")
|
||||
else:
|
||||
messages.error(request, f"Failed to start workflow for {kyc_request.uuid}")
|
||||
messages.error(request, f"Failed to start workflow for {kyc_app.uuid}")
|
||||
|
||||
|
||||
@admin.register(KYCRequest)
|
||||
class KYCRequestAdmin(admin.ModelAdmin):
|
||||
@admin.register(KYCApplication)
|
||||
class KYCApplicationAdmin(admin.ModelAdmin):
|
||||
list_display = ('uuid', 'user_id', 'team_name', 'country_code', 'workflow_status', 'contact_person', 'created_at')
|
||||
list_filter = ('workflow_status', 'country_code', 'created_at')
|
||||
search_fields = ('uuid', 'user_id', 'team_name', 'contact_email', 'contact_person')
|
||||
@@ -40,8 +40,8 @@ class KYCRequestAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
|
||||
@admin.register(KYCMonitoring)
|
||||
class KYCMonitoringAdmin(admin.ModelAdmin):
|
||||
@admin.register(KYCProfile)
|
||||
class KYCProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ('uuid', 'user_id', 'team_name', 'country_code', 'workflow_status', 'contact_person', 'created_at')
|
||||
list_filter = ('workflow_status', 'country_code', 'created_at')
|
||||
search_fields = ('uuid', 'user_id', 'team_name', 'contact_email', 'contact_person')
|
||||
@@ -65,8 +65,8 @@ class KYCMonitoringAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
|
||||
@admin.register(KYCRequestRussia)
|
||||
class KYCRequestRussiaAdmin(admin.ModelAdmin):
|
||||
@admin.register(KYCDetailsRussia)
|
||||
class KYCDetailsRussiaAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'company_name', 'inn', 'ogrn')
|
||||
search_fields = ('company_name', 'inn', 'ogrn')
|
||||
ordering = ('-id',)
|
||||
|
||||
@@ -35,7 +35,7 @@ class AbstractKycBase(models.Model):
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='kyc_requests'
|
||||
related_name='%(class)s_set'
|
||||
)
|
||||
object_id = models.PositiveIntegerField(null=True, blank=True)
|
||||
country_details = GenericForeignKey('content_type', 'object_id')
|
||||
@@ -49,24 +49,16 @@ class AbstractKycBase(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class KYCRequest(AbstractKycBase):
|
||||
"""Главная модель KYC заявки. Ссылается на детали страны через ContentType."""
|
||||
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='kyc_requests'
|
||||
)
|
||||
object_id = models.PositiveIntegerField(null=True, blank=True)
|
||||
country_details = GenericForeignKey('content_type', 'object_id')
|
||||
class KYCApplication(AbstractKycBase):
|
||||
"""KYC заявка на верификацию. Одноразовый процесс."""
|
||||
|
||||
class Meta:
|
||||
db_table = 'kyc_requests'
|
||||
db_table = 'kyc_requests' # Сохраняем для совместимости
|
||||
verbose_name = 'KYC Application'
|
||||
verbose_name_plural = 'KYC Applications'
|
||||
|
||||
def __str__(self):
|
||||
return f"KYC {self.user_id} - {self.workflow_status}"
|
||||
return f"KYC Application {self.user_id} - {self.workflow_status}"
|
||||
|
||||
def get_country_data(self) -> dict:
|
||||
"""Получить данные страны как словарь для Temporal workflow."""
|
||||
@@ -75,27 +67,28 @@ class KYCRequest(AbstractKycBase):
|
||||
return {}
|
||||
|
||||
|
||||
class KYCMonitoring(AbstractKycBase):
|
||||
"""KYC мониторинг (копия заявки для долгосрочного наблюдения)."""
|
||||
class KYCProfile(AbstractKycBase):
|
||||
"""KYC профиль компании для долгосрочного мониторинга.
|
||||
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='kyc_monitoring'
|
||||
)
|
||||
object_id = models.PositiveIntegerField(null=True, blank=True)
|
||||
country_details = GenericForeignKey('content_type', 'object_id')
|
||||
Создается после успешной верификации (approval) заявки.
|
||||
Используется Dagster для сбора данных о компании.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
db_table = 'kyc_monitoring'
|
||||
db_table = 'kyc_monitoring' # Сохраняем для совместимости
|
||||
verbose_name = 'KYC Profile'
|
||||
verbose_name_plural = 'KYC Profiles'
|
||||
|
||||
def __str__(self):
|
||||
return f"KYC Monitoring {self.user_id} - {self.workflow_status}"
|
||||
return f"KYC Profile {self.user_id} - {self.workflow_status}"
|
||||
|
||||
|
||||
class KYCRequestRussia(models.Model):
|
||||
# Aliases for backwards compatibility
|
||||
KYCRequest = KYCApplication
|
||||
KYCMonitoring = KYCProfile
|
||||
|
||||
|
||||
class KYCDetailsRussia(models.Model):
|
||||
"""Детали KYC для России. Отдельная модель без наследования."""
|
||||
|
||||
# Данные компании от DaData
|
||||
@@ -113,6 +106,8 @@ class KYCRequestRussia(models.Model):
|
||||
|
||||
class Meta:
|
||||
db_table = 'kyc_details_russia'
|
||||
verbose_name = 'KYC Details Russia'
|
||||
verbose_name_plural = 'KYC Details Russia'
|
||||
|
||||
def __str__(self):
|
||||
return f"KYC Russia: {self.company_name} (ИНН: {self.inn})"
|
||||
@@ -130,3 +125,7 @@ class KYCRequestRussia(models.Model):
|
||||
"bik": self.bik,
|
||||
"correspondent_account": self.correspondent_account,
|
||||
}
|
||||
|
||||
|
||||
# Alias for backwards compatibility
|
||||
KYCRequestRussia = KYCDetailsRussia
|
||||
|
||||
98
kyc_app/schemas/m2m_schema.py
Normal file
98
kyc_app/schemas/m2m_schema.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
M2M GraphQL Schema for internal service-to-service calls.
|
||||
|
||||
This endpoint is called by Temporal worker to create KYCProfile records.
|
||||
No authentication required (internal network only).
|
||||
"""
|
||||
|
||||
import graphene
|
||||
|
||||
from ..models import KYCApplication, KYCProfile
|
||||
|
||||
|
||||
class CreateKycProfileMutation(graphene.Mutation):
|
||||
"""Create KYCProfile record from existing KYCApplication.
|
||||
|
||||
Called after KYC approval to create long-term monitoring profile.
|
||||
"""
|
||||
|
||||
class Arguments:
|
||||
kyc_application_id = graphene.String(required=True)
|
||||
|
||||
success = graphene.Boolean()
|
||||
profile_uuid = graphene.String()
|
||||
message = graphene.String()
|
||||
|
||||
def mutate(self, info, kyc_application_id: str):
|
||||
try:
|
||||
# Find the KYCApplication
|
||||
kyc_application = KYCApplication.objects.get(uuid=kyc_application_id)
|
||||
|
||||
# Check if profile already exists for this user/team
|
||||
existing = KYCProfile.objects.filter(
|
||||
user_id=kyc_application.user_id,
|
||||
team_name=kyc_application.team_name,
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return CreateKycProfileMutation(
|
||||
success=True,
|
||||
profile_uuid=str(existing.uuid),
|
||||
message="Profile already exists",
|
||||
)
|
||||
|
||||
# Create KYCProfile by copying fields from KYCApplication
|
||||
profile = KYCProfile.objects.create(
|
||||
user_id=kyc_application.user_id,
|
||||
team_name=kyc_application.team_name,
|
||||
country_code=kyc_application.country_code,
|
||||
workflow_status='active', # Approved = active for profile
|
||||
score=kyc_application.score,
|
||||
contact_person=kyc_application.contact_person,
|
||||
contact_email=kyc_application.contact_email,
|
||||
contact_phone=kyc_application.contact_phone,
|
||||
content_type=kyc_application.content_type,
|
||||
object_id=kyc_application.object_id,
|
||||
approved_by=kyc_application.approved_by,
|
||||
approved_at=kyc_application.approved_at,
|
||||
)
|
||||
|
||||
return CreateKycProfileMutation(
|
||||
success=True,
|
||||
profile_uuid=str(profile.uuid),
|
||||
message="Profile created",
|
||||
)
|
||||
|
||||
except KYCApplication.DoesNotExist:
|
||||
return CreateKycProfileMutation(
|
||||
success=False,
|
||||
profile_uuid="",
|
||||
message=f"KYCApplication not found: {kyc_application_id}",
|
||||
)
|
||||
except Exception as e:
|
||||
return CreateKycProfileMutation(
|
||||
success=False,
|
||||
profile_uuid="",
|
||||
message=str(e),
|
||||
)
|
||||
|
||||
|
||||
class M2MQuery(graphene.ObjectType):
|
||||
"""M2M Query - health check only."""
|
||||
|
||||
health = graphene.String()
|
||||
|
||||
def resolve_health(self, info):
|
||||
return "ok"
|
||||
|
||||
|
||||
class M2MMutation(graphene.ObjectType):
|
||||
"""M2M Mutations for internal service calls."""
|
||||
|
||||
# New name
|
||||
create_kyc_profile = CreateKycProfileMutation.Field()
|
||||
# Old name for backwards compatibility
|
||||
create_kyc_monitoring = CreateKycProfileMutation.Field()
|
||||
|
||||
|
||||
m2m_schema = graphene.Schema(query=M2MQuery, mutation=M2MMutation)
|
||||
264
kyc_app/schemas/public_schema.py
Normal file
264
kyc_app/schemas/public_schema.py
Normal file
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
Public GraphQL Schema for company data.
|
||||
|
||||
This endpoint provides:
|
||||
- companyTeaser: public data (no auth required)
|
||||
- companyFull: full data (requires authentication)
|
||||
|
||||
Data is read from MongoDB company_documents collection.
|
||||
Queries can be by INN (direct) or by KYC Profile UUID.
|
||||
"""
|
||||
|
||||
import graphene
|
||||
from django.conf import settings
|
||||
from pymongo import MongoClient
|
||||
|
||||
from ..models import KYCProfile, KYCDetailsRussia
|
||||
|
||||
|
||||
def get_mongo_client():
|
||||
"""Get MongoDB client."""
|
||||
if not settings.MONGODB_URI:
|
||||
return None
|
||||
return MongoClient(settings.MONGODB_URI)
|
||||
|
||||
|
||||
def get_company_documents(inn: str) -> list:
|
||||
"""Get all documents for a company by INN."""
|
||||
client = get_mongo_client()
|
||||
if not client:
|
||||
return []
|
||||
|
||||
try:
|
||||
db = client[settings.MONGODB_DB]
|
||||
collection = db["company_documents"]
|
||||
return list(collection.find({"inn": inn}))
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def get_inn_by_profile_uuid(profile_uuid: str) -> str | None:
|
||||
"""Get INN from KYCProfile by its UUID."""
|
||||
try:
|
||||
profile = KYCProfile.objects.get(uuid=profile_uuid)
|
||||
# Get country details (KYCDetailsRussia) via GenericForeignKey
|
||||
if profile.country_details and isinstance(profile.country_details, KYCDetailsRussia):
|
||||
return profile.country_details.inn
|
||||
return None
|
||||
except KYCProfile.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def aggregate_company_data(documents: list) -> dict:
|
||||
"""Aggregate data from multiple source documents into summary."""
|
||||
if not documents:
|
||||
return {}
|
||||
|
||||
summary = {
|
||||
"inn": None,
|
||||
"ogrn": None,
|
||||
"name": None,
|
||||
"company_type": None,
|
||||
"registration_year": None,
|
||||
"is_active": True,
|
||||
"address": None,
|
||||
"director": None,
|
||||
"capital": None,
|
||||
"activities": [],
|
||||
"sources": [],
|
||||
"last_updated": None,
|
||||
}
|
||||
|
||||
for doc in documents:
|
||||
source = doc.get("source", "unknown")
|
||||
summary["sources"].append(source)
|
||||
|
||||
data = doc.get("data", {})
|
||||
|
||||
# Extract common fields
|
||||
if not summary["inn"]:
|
||||
summary["inn"] = doc.get("inn")
|
||||
|
||||
if not summary["ogrn"] and data.get("ogrn"):
|
||||
summary["ogrn"] = data["ogrn"]
|
||||
|
||||
if not summary["name"] and data.get("name"):
|
||||
summary["name"] = data["name"]
|
||||
|
||||
# Parse company type from name
|
||||
if not summary["company_type"] and summary["name"]:
|
||||
name = summary["name"].upper()
|
||||
if "ООО" in name or "ОБЩЕСТВО С ОГРАНИЧЕННОЙ" in name:
|
||||
summary["company_type"] = "ООО"
|
||||
elif "АО" in name or "АКЦИОНЕРНОЕ ОБЩЕСТВО" in name:
|
||||
summary["company_type"] = "АО"
|
||||
elif "ИП" in name or "ИНДИВИДУАЛЬНЫЙ ПРЕДПРИНИМАТЕЛЬ" in name:
|
||||
summary["company_type"] = "ИП"
|
||||
elif "ПАО" in name:
|
||||
summary["company_type"] = "ПАО"
|
||||
|
||||
# Track last update
|
||||
collected_at = doc.get("collected_at")
|
||||
if collected_at:
|
||||
if not summary["last_updated"] or collected_at > summary["last_updated"]:
|
||||
summary["last_updated"] = collected_at
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
class CompanyTeaserType(graphene.ObjectType):
|
||||
"""Public company data (teaser)."""
|
||||
|
||||
company_type = graphene.String(description="Company type: ООО, АО, ИП, etc.")
|
||||
registration_year = graphene.Int(description="Year of registration")
|
||||
is_active = graphene.Boolean(description="Is company active")
|
||||
sources_count = graphene.Int(description="Number of data sources")
|
||||
|
||||
|
||||
class CompanyFullType(graphene.ObjectType):
|
||||
"""Full company data (requires auth)."""
|
||||
|
||||
inn = graphene.String()
|
||||
ogrn = graphene.String()
|
||||
name = graphene.String()
|
||||
company_type = graphene.String()
|
||||
registration_year = graphene.Int()
|
||||
is_active = graphene.Boolean()
|
||||
address = graphene.String()
|
||||
director = graphene.String()
|
||||
capital = graphene.String()
|
||||
activities = graphene.List(graphene.String)
|
||||
sources = graphene.List(graphene.String)
|
||||
last_updated = graphene.DateTime()
|
||||
|
||||
|
||||
class PublicQuery(graphene.ObjectType):
|
||||
"""Public queries - no authentication required."""
|
||||
|
||||
# Query by KYC Profile UUID (preferred - used by frontend)
|
||||
company_teaser_by_profile = graphene.Field(
|
||||
CompanyTeaserType,
|
||||
profile_uuid=graphene.String(required=True),
|
||||
description="Get public company teaser data by KYC Profile UUID",
|
||||
)
|
||||
|
||||
company_full_by_profile = graphene.Field(
|
||||
CompanyFullType,
|
||||
profile_uuid=graphene.String(required=True),
|
||||
description="Get full company data by KYC Profile UUID (requires auth)",
|
||||
)
|
||||
|
||||
# Query by INN (legacy/direct)
|
||||
company_teaser = graphene.Field(
|
||||
CompanyTeaserType,
|
||||
inn=graphene.String(required=True),
|
||||
description="Get public company teaser data by INN",
|
||||
)
|
||||
|
||||
company_full = graphene.Field(
|
||||
CompanyFullType,
|
||||
inn=graphene.String(required=True),
|
||||
description="Get full company data by INN (requires auth)",
|
||||
)
|
||||
|
||||
health = graphene.String()
|
||||
|
||||
def resolve_health(self, info):
|
||||
return "ok"
|
||||
|
||||
def resolve_company_teaser_by_profile(self, info, profile_uuid: str):
|
||||
"""Return public teaser data by KYC Profile UUID."""
|
||||
inn = get_inn_by_profile_uuid(profile_uuid)
|
||||
if not inn:
|
||||
return None
|
||||
|
||||
documents = get_company_documents(inn)
|
||||
if not documents:
|
||||
return None
|
||||
|
||||
summary = aggregate_company_data(documents)
|
||||
|
||||
return CompanyTeaserType(
|
||||
company_type=summary.get("company_type"),
|
||||
registration_year=summary.get("registration_year"),
|
||||
is_active=summary.get("is_active", True),
|
||||
sources_count=len(summary.get("sources", [])),
|
||||
)
|
||||
|
||||
def resolve_company_full_by_profile(self, info, profile_uuid: str):
|
||||
"""Return full company data by KYC Profile UUID (requires auth)."""
|
||||
# Check authentication
|
||||
user_id = getattr(info.context, 'user_id', None)
|
||||
if not user_id:
|
||||
return None # Not authenticated
|
||||
|
||||
inn = get_inn_by_profile_uuid(profile_uuid)
|
||||
if not inn:
|
||||
return None
|
||||
|
||||
documents = get_company_documents(inn)
|
||||
if not documents:
|
||||
return None
|
||||
|
||||
summary = aggregate_company_data(documents)
|
||||
|
||||
return CompanyFullType(
|
||||
inn=summary.get("inn"),
|
||||
ogrn=summary.get("ogrn"),
|
||||
name=summary.get("name"),
|
||||
company_type=summary.get("company_type"),
|
||||
registration_year=summary.get("registration_year"),
|
||||
is_active=summary.get("is_active", True),
|
||||
address=summary.get("address"),
|
||||
director=summary.get("director"),
|
||||
capital=summary.get("capital"),
|
||||
activities=summary.get("activities", []),
|
||||
sources=summary.get("sources", []),
|
||||
last_updated=summary.get("last_updated"),
|
||||
)
|
||||
|
||||
def resolve_company_teaser(self, info, inn: str):
|
||||
"""Return public teaser data by INN (no auth required)."""
|
||||
documents = get_company_documents(inn)
|
||||
if not documents:
|
||||
return None
|
||||
|
||||
summary = aggregate_company_data(documents)
|
||||
|
||||
return CompanyTeaserType(
|
||||
company_type=summary.get("company_type"),
|
||||
registration_year=summary.get("registration_year"),
|
||||
is_active=summary.get("is_active", True),
|
||||
sources_count=len(summary.get("sources", [])),
|
||||
)
|
||||
|
||||
def resolve_company_full(self, info, inn: str):
|
||||
"""Return full company data by INN (requires auth)."""
|
||||
# Check authentication
|
||||
user_id = getattr(info.context, 'user_id', None)
|
||||
if not user_id:
|
||||
return None # Not authenticated
|
||||
|
||||
documents = get_company_documents(inn)
|
||||
if not documents:
|
||||
return None
|
||||
|
||||
summary = aggregate_company_data(documents)
|
||||
|
||||
return CompanyFullType(
|
||||
inn=summary.get("inn"),
|
||||
ogrn=summary.get("ogrn"),
|
||||
name=summary.get("name"),
|
||||
company_type=summary.get("company_type"),
|
||||
registration_year=summary.get("registration_year"),
|
||||
is_active=summary.get("is_active", True),
|
||||
address=summary.get("address"),
|
||||
director=summary.get("director"),
|
||||
capital=summary.get("capital"),
|
||||
activities=summary.get("activities", []),
|
||||
sources=summary.get("sources", []),
|
||||
last_updated=summary.get("last_updated"),
|
||||
)
|
||||
|
||||
|
||||
public_schema = graphene.Schema(query=PublicQuery)
|
||||
@@ -1,13 +1,14 @@
|
||||
import graphene
|
||||
from graphene_django import DjangoObjectType
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from ..models import KYCRequest, KYCRequestRussia
|
||||
from ..models import KYCApplication, KYCDetailsRussia
|
||||
from ..temporal import KycWorkflowClient
|
||||
|
||||
|
||||
class KYCRequestType(DjangoObjectType):
|
||||
class KYCApplicationType(DjangoObjectType):
|
||||
"""GraphQL type for KYC Application (заявка)."""
|
||||
class Meta:
|
||||
model = KYCRequest
|
||||
model = KYCApplication
|
||||
fields = '__all__'
|
||||
|
||||
country_data = graphene.JSONString()
|
||||
@@ -17,13 +18,15 @@ class KYCRequestType(DjangoObjectType):
|
||||
return self.get_country_data()
|
||||
|
||||
|
||||
class KYCRequestRussiaType(DjangoObjectType):
|
||||
class KYCDetailsRussiaType(DjangoObjectType):
|
||||
"""GraphQL type for Russia-specific KYC details."""
|
||||
class Meta:
|
||||
model = KYCRequestRussia
|
||||
model = KYCDetailsRussia
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class KYCRequestRussiaInput(graphene.InputObjectType):
|
||||
class KYCApplicationRussiaInput(graphene.InputObjectType):
|
||||
"""Input for creating KYC Application for Russia."""
|
||||
companyName = graphene.String(required=True)
|
||||
companyFullName = graphene.String(required=True)
|
||||
inn = graphene.String(required=True)
|
||||
@@ -38,11 +41,12 @@ class KYCRequestRussiaInput(graphene.InputObjectType):
|
||||
contactPhone = graphene.String(required=True)
|
||||
|
||||
|
||||
class CreateKYCRequestRussia(graphene.Mutation):
|
||||
class CreateKYCApplicationRussia(graphene.Mutation):
|
||||
"""Create KYC Application for Russian company."""
|
||||
class Arguments:
|
||||
input = KYCRequestRussiaInput(required=True)
|
||||
input = KYCApplicationRussiaInput(required=True)
|
||||
|
||||
kyc_request = graphene.Field(KYCRequestType)
|
||||
kyc_application = graphene.Field(KYCApplicationType)
|
||||
success = graphene.Boolean()
|
||||
|
||||
def mutate(self, info, input):
|
||||
@@ -52,7 +56,7 @@ class CreateKYCRequestRussia(graphene.Mutation):
|
||||
raise Exception("Not authenticated")
|
||||
|
||||
# 1. Create Russia details
|
||||
russia_details = KYCRequestRussia.objects.create(
|
||||
russia_details = KYCDetailsRussia.objects.create(
|
||||
company_name=input.companyName,
|
||||
company_full_name=input.companyFullName,
|
||||
inn=input.inn,
|
||||
@@ -64,49 +68,67 @@ class CreateKYCRequestRussia(graphene.Mutation):
|
||||
correspondent_account=input.correspondentAccount or '',
|
||||
)
|
||||
|
||||
# 2. Create main KYCRequest with reference to details
|
||||
kyc_request = KYCRequest.objects.create(
|
||||
# 2. Create main KYC Application with reference to details
|
||||
kyc_application = KYCApplication.objects.create(
|
||||
user_id=user_id,
|
||||
team_name=input.companyName,
|
||||
country_code='RU',
|
||||
contact_person=input.contactPerson,
|
||||
contact_email=input.contactEmail,
|
||||
contact_phone=input.contactPhone,
|
||||
content_type=ContentType.objects.get_for_model(KYCRequestRussia),
|
||||
content_type=ContentType.objects.get_for_model(KYCDetailsRussia),
|
||||
object_id=russia_details.id,
|
||||
)
|
||||
|
||||
# 3. Start Temporal workflow
|
||||
KycWorkflowClient.start(kyc_request)
|
||||
KycWorkflowClient.start(kyc_application)
|
||||
|
||||
return CreateKYCRequestRussia(kyc_request=kyc_request, success=True)
|
||||
return CreateKYCApplicationRussia(kyc_application=kyc_application, success=True)
|
||||
|
||||
|
||||
class UserQuery(graphene.ObjectType):
|
||||
"""User schema - ID token authentication"""
|
||||
kyc_requests = graphene.List(KYCRequestType)
|
||||
kyc_request = graphene.Field(KYCRequestType, uuid=graphene.String(required=True))
|
||||
# Keep old names for backwards compatibility
|
||||
kyc_requests = graphene.List(KYCApplicationType, description="Get user's KYC applications")
|
||||
kyc_request = graphene.Field(KYCApplicationType, uuid=graphene.String(required=True))
|
||||
# New names
|
||||
kyc_applications = graphene.List(KYCApplicationType, description="Get user's KYC applications")
|
||||
kyc_application = graphene.Field(KYCApplicationType, uuid=graphene.String(required=True))
|
||||
|
||||
def resolve_kyc_requests(self, info):
|
||||
# Filter by user_id from JWT token
|
||||
return self._get_applications(info)
|
||||
|
||||
def resolve_kyc_applications(self, info):
|
||||
return self._get_applications(info)
|
||||
|
||||
def _get_applications(self, info):
|
||||
user_id = getattr(info.context, 'user_id', None)
|
||||
if not user_id:
|
||||
return []
|
||||
return KYCRequest.objects.filter(user_id=user_id)
|
||||
return KYCApplication.objects.filter(user_id=user_id)
|
||||
|
||||
def resolve_kyc_request(self, info, uuid):
|
||||
return self._get_application(info, uuid)
|
||||
|
||||
def resolve_kyc_application(self, info, uuid):
|
||||
return self._get_application(info, uuid)
|
||||
|
||||
def _get_application(self, info, uuid):
|
||||
user_id = getattr(info.context, 'user_id', None)
|
||||
if not user_id:
|
||||
return None
|
||||
try:
|
||||
return KYCRequest.objects.get(uuid=uuid, user_id=user_id)
|
||||
except KYCRequest.DoesNotExist:
|
||||
return KYCApplication.objects.get(uuid=uuid, user_id=user_id)
|
||||
except KYCApplication.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class UserMutation(graphene.ObjectType):
|
||||
"""User mutations - ID token authentication"""
|
||||
create_kyc_request_russia = CreateKYCRequestRussia.Field()
|
||||
# Keep old name for backwards compatibility
|
||||
create_kyc_request_russia = CreateKYCApplicationRussia.Field()
|
||||
# New name
|
||||
create_kyc_application_russia = CreateKYCApplicationRussia.Field()
|
||||
|
||||
|
||||
user_schema = graphene.Schema(query=UserQuery, mutation=UserMutation)
|
||||
|
||||
@@ -13,3 +13,37 @@ class UserGraphQLView(GraphQLView):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['middleware'] = [UserJWTMiddleware()]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class M2MGraphQLView(GraphQLView):
|
||||
"""M2M endpoint - no authentication (internal network only)."""
|
||||
pass
|
||||
|
||||
|
||||
class OptionalUserJWTMiddleware:
|
||||
"""Middleware that optionally extracts user_id but doesn't fail if missing."""
|
||||
|
||||
def resolve(self, next, root, info, **args):
|
||||
request = info.context
|
||||
|
||||
# Try to extract user_id from Authorization header if present
|
||||
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
||||
if auth_header.startswith('Bearer '):
|
||||
try:
|
||||
from .auth import validate_jwt_token
|
||||
token = auth_header.split(' ', 1)[1]
|
||||
payload = validate_jwt_token(token)
|
||||
if payload:
|
||||
request.user_id = payload.get('sub')
|
||||
except Exception:
|
||||
pass # Ignore auth errors - user just won't get full data
|
||||
|
||||
return next(root, info, **args)
|
||||
|
||||
|
||||
class PublicGraphQLView(GraphQLView):
|
||||
"""Public endpoint - optional auth for full data, no auth for teaser."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Use optional auth middleware that doesn't fail on missing token
|
||||
kwargs['middleware'] = [OptionalUserJWTMiddleware()]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
Reference in New Issue
Block a user