feat: add get me

This commit is contained in:
2026-05-12 20:48:06 +03:00
parent 1a76aa5a66
commit d9ecdd7b86
8 changed files with 88 additions and 105 deletions

View File

@@ -5,12 +5,12 @@ services:
context: .
dockerfile: Dockerfile
ports:
- "8002:8002"
- "8003:8003"
environment:
PYTHONUNBUFFERED: "1"
APP_MODULE: "src.main:app"
APP_HOST: "0.0.0.0"
APP_PORT: "8002"
APP_PORT: "8003"
APP_WORKERS: "1"
env_file:
- .env
@@ -52,32 +52,6 @@ services:
timeout: 2s
retries: 20
# keydb:
# image: eqalpha/keydb
# container_name: keydb
# restart: no
# expose:
# - "6379"
# volumes:
# - keydb_data:/data
# environment:
# KEYDB_PASSWORD: keydb
# command: >
# sh -c "
# keydb-server
# --requirepass $$KEYDB_PASSWORD
# --dir /data
# --appendonly yes
# --appendfsync everysec
# --save 900 1
# --save 300 10
# --save 60 10000
# "
# healthcheck:
# test: ["CMD", "redis-cli", "ping"]
# interval: 5s
# timeout: 2s
# retries: 20
volumes:
keydb_data:

View File

@@ -24,9 +24,9 @@ class UpdateBankDetailsCompleteCommand:
*,
user_id: str,
code: str,
bik: str | None = None,
account_number: str | None = None,
card_number: str | None = None,
passport_data: str | None = None,
inn: str | None = None,
erc20: str | None = None,
) -> UserEntity:
code = (code or '').strip()
@@ -56,12 +56,12 @@ class UpdateBankDetailsCompleteCommand:
raise ApplicationException(400, 'Invalid or expired code')
fields = {}
if bik is not None:
fields['bik'] = bik
if account_number is not None:
fields['account_number'] = account_number
if card_number is not None:
fields['card_number'] = card_number
if passport_data is not None:
fields['passport_data'] = passport_data
if inn is not None:
fields['inn'] = inn
if erc20 is not None:
fields['erc20'] = erc20
user = await self._unit_of_work.user_repository.set_bank_details(user_id, **fields)
await self._cache.set_user(user_id, user)

View File

@@ -17,10 +17,9 @@ class UserEntity:
crypto_wallet: str | None = None
phone: str | None = None
bik: str | None = None
account_number: str | None = None
card_number: str | None = None
passport_data: str | None = None
inn: str | None = None
erc20: str | None = None
kyc_verified: bool | None = None
is_deleted: bool | None = None

View File

@@ -39,10 +39,9 @@ class KeydbCache(ICache):
'birth_date': str(user.birth_date) if user.birth_date else None,
'crypto_wallet': user.crypto_wallet,
'phone': user.phone,
'bik': user.bik,
'account_number': user.account_number,
'card_number': user.card_number,
'passport_data': user.passport_data,
'inn': user.inn,
'erc20': user.erc20,
'kyc_verified': user.kyc_verified,
'is_deleted': user.is_deleted,
'created_at': user.created_at.isoformat() if user.created_at else None,

View File

@@ -42,10 +42,9 @@ class UserRepository(IUserRepository):
birth_date=user.birth_date,
crypto_wallet=user.crypto_wallet,
phone=user.phone,
bik=user.bik,
account_number=user.account_number,
card_number=user.card_number,
passport_data=user.passport_data,
inn=user.inn,
erc20=user.erc20,
kyc_verified_at=user.kyc_verified_at,
kyc_verified=user.kyc_verified,
is_deleted=user.is_deleted,
@@ -81,7 +80,14 @@ class UserRepository(IUserRepository):
return await self._update_field(user_id, phone=phone)
async def set_bank_details(self, user_id: str, **fields: str) -> UserEntity:
return await self._update_field(user_id, **fields)
allowed = {'passport_data', 'inn', 'erc20'}
payload = {k: v for k, v in fields.items() if k in allowed and v is not None}
if not payload:
raise ApplicationException(
status_code=status.HTTP_400_BAD_REQUEST,
message='No identity fields to update',
)
return await self._update_field(user_id, **payload)
async def set_crypto_wallet(self, user_id: str, wallet_address: str) -> UserEntity:
return await self._update_field(user_id, crypto_wallet=wallet_address)

View File

@@ -30,10 +30,9 @@ async def me(
'birth_date': str(user.birth_date) if user.birth_date else None,
'crypto_wallet': user.crypto_wallet,
'phone': user.phone,
'bik': user.bik,
'account_number': user.account_number,
'card_number': user.card_number,
'passport_data': user.passport_data,
'inn': user.inn,
'erc20': user.erc20,
'kyc_verified': user.kyc_verified,
'is_deleted': user.is_deleted,
'created_at': user.created_at.isoformat() if user.created_at else None,

View File

@@ -136,15 +136,15 @@ async def bank_details_complete(
user = await command(
user_id=auth.user_id,
code=body.code,
bik=body.bik,
account_number=body.account_number,
card_number=body.card_number,
passport_data=body.passport_data,
inn=body.inn,
erc20=body.erc20,
)
return ORJSONResponse(
status_code=status.HTTP_200_OK,
content={
'bik': user.bik,
'account_number': user.account_number,
'card_number': user.card_number,
'passport_data': user.passport_data,
'inn': user.inn,
'erc20': user.erc20,
},
)

View File

@@ -4,69 +4,67 @@ from pydantic import BaseModel, field_validator, model_validator
class BankUpdateRequest(BaseModel):
bik: str | None = None
account_number: str | None = None
card_number: str | None = None
passport_data: str | None = None
inn: str | None = None
erc20: str | None = None
@model_validator(mode='after')
def at_least_one(self) -> Self:
if not any([self.bik, self.account_number, self.card_number]):
if not any([self.passport_data, self.inn, self.erc20]):
raise ValueError('At least one field is required')
return self
@field_validator('bik')
@field_validator('passport_data', 'inn', 'erc20')
@classmethod
def validate_bik(cls, v: str | None) -> str | None:
def strip_optional(cls, v: str | None) -> str | None:
if v is None:
return None
s = v.strip()
return s or None
@field_validator('inn')
@classmethod
def validate_inn(cls, v: str | None) -> str | None:
if v is None:
return None
v = v.strip()
if not re.match(r'^\d{9}$', v):
raise ValueError('BIK must be exactly 9 digits')
if not re.match(r'^\d{10}(\d{2})?$', v):
raise ValueError('INN must be 10 or 12 digits')
if len(v) > 12:
raise ValueError('INN is too long')
return v
@field_validator('account_number')
@field_validator('erc20')
@classmethod
def validate_account_number(cls, v: str | None) -> str | None:
def validate_erc20(cls, v: str | None) -> str | None:
if v is None:
return None
v = v.strip()
if not re.match(r'^\d{20}$', v):
raise ValueError('Account number must be exactly 20 digits')
if not re.match(r'^0x[a-fA-F0-9]{40}$', v):
raise ValueError('ERC20 address must be 0x followed by 40 hex characters')
return v
@field_validator('card_number')
@field_validator('passport_data')
@classmethod
def validate_card_number(cls, v: str | None) -> str | None:
def validate_passport_data(cls, v: str | None) -> str | None:
if v is None:
return None
v = re.sub(r'[\s\-]', '', v)
if not re.match(r'^\d{13,19}$', v):
raise ValueError('Card number must be 13-19 digits')
if not cls._luhn_check(v):
raise ValueError('Invalid card number (Luhn check failed)')
v = v.strip()
if len(v) > 255:
raise ValueError('Passport data is too long')
return v
@staticmethod
def _luhn_check(number: str) -> bool:
digits = [int(d) for d in number]
odd_digits = digits[-1::-2]
even_digits = digits[-2::-2]
total = sum(odd_digits)
for d in even_digits:
total += sum(divmod(d * 2, 10))
return total % 10 == 0
class BankConfirmRequest(BaseModel):
code: str
bik: str | None = None
account_number: str | None = None
card_number: str | None = None
passport_data: str | None = None
inn: str | None = None
erc20: str | None = None
@model_validator(mode='after')
def at_least_one_field(self) -> Self:
if not any([self.bik, self.account_number, self.card_number]):
raise ValueError('At least one bank field is required')
if not any([self.passport_data, self.inn, self.erc20]):
raise ValueError('At least one field is required')
return self
@field_validator('code')
@@ -77,34 +75,42 @@ class BankConfirmRequest(BaseModel):
raise ValueError('Code must be exactly 6 digits')
return v
@field_validator('bik')
@field_validator('passport_data', 'inn', 'erc20')
@classmethod
def validate_bik(cls, v: str | None) -> str | None:
def strip_optional(cls, v: str | None) -> str | None:
if v is None:
return None
s = v.strip()
return s or None
@field_validator('inn')
@classmethod
def validate_inn(cls, v: str | None) -> str | None:
if v is None:
return None
v = v.strip()
if not re.match(r'^\d{9}$', v):
raise ValueError('BIK must be exactly 9 digits')
if not re.match(r'^\d{10}(\d{2})?$', v):
raise ValueError('INN must be 10 or 12 digits')
if len(v) > 12:
raise ValueError('INN is too long')
return v
@field_validator('account_number')
@field_validator('erc20')
@classmethod
def validate_account_number(cls, v: str | None) -> str | None:
def validate_erc20(cls, v: str | None) -> str | None:
if v is None:
return None
v = v.strip()
if not re.match(r'^\d{20}$', v):
raise ValueError('Account number must be exactly 20 digits')
if not re.match(r'^0x[a-fA-F0-9]{40}$', v):
raise ValueError('ERC20 address must be 0x followed by 40 hex characters')
return v
@field_validator('card_number')
@field_validator('passport_data')
@classmethod
def validate_card_number(cls, v: str | None) -> str | None:
def validate_passport_data(cls, v: str | None) -> str | None:
if v is None:
return None
v = re.sub(r'[\s\-]', '', v)
if not re.match(r'^\d{13,19}$', v):
raise ValueError('Card number must be 13-19 digits')
if not BankUpdateRequest._luhn_check(v):
raise ValueError('Invalid card number (Luhn check failed)')
v = v.strip()
if len(v) > 255:
raise ValueError('Passport data is too long')
return v