feat: add get me
This commit is contained in:
@@ -5,12 +5,12 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8002:8002"
|
- "8003:8003"
|
||||||
environment:
|
environment:
|
||||||
PYTHONUNBUFFERED: "1"
|
PYTHONUNBUFFERED: "1"
|
||||||
APP_MODULE: "src.main:app"
|
APP_MODULE: "src.main:app"
|
||||||
APP_HOST: "0.0.0.0"
|
APP_HOST: "0.0.0.0"
|
||||||
APP_PORT: "8002"
|
APP_PORT: "8003"
|
||||||
APP_WORKERS: "1"
|
APP_WORKERS: "1"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -52,32 +52,6 @@ services:
|
|||||||
timeout: 2s
|
timeout: 2s
|
||||||
retries: 20
|
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:
|
volumes:
|
||||||
keydb_data:
|
keydb_data:
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ class UpdateBankDetailsCompleteCommand:
|
|||||||
*,
|
*,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
code: str,
|
code: str,
|
||||||
bik: str | None = None,
|
passport_data: str | None = None,
|
||||||
account_number: str | None = None,
|
inn: str | None = None,
|
||||||
card_number: str | None = None,
|
erc20: str | None = None,
|
||||||
) -> UserEntity:
|
) -> UserEntity:
|
||||||
code = (code or '').strip()
|
code = (code or '').strip()
|
||||||
|
|
||||||
@@ -56,12 +56,12 @@ class UpdateBankDetailsCompleteCommand:
|
|||||||
raise ApplicationException(400, 'Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
if bik is not None:
|
if passport_data is not None:
|
||||||
fields['bik'] = bik
|
fields['passport_data'] = passport_data
|
||||||
if account_number is not None:
|
if inn is not None:
|
||||||
fields['account_number'] = account_number
|
fields['inn'] = inn
|
||||||
if card_number is not None:
|
if erc20 is not None:
|
||||||
fields['card_number'] = card_number
|
fields['erc20'] = erc20
|
||||||
|
|
||||||
user = await self._unit_of_work.user_repository.set_bank_details(user_id, **fields)
|
user = await self._unit_of_work.user_repository.set_bank_details(user_id, **fields)
|
||||||
await self._cache.set_user(user_id, user)
|
await self._cache.set_user(user_id, user)
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ class UserEntity:
|
|||||||
crypto_wallet: str | None = None
|
crypto_wallet: str | None = None
|
||||||
phone: str | None = None
|
phone: str | None = None
|
||||||
|
|
||||||
bik: str | None = None
|
passport_data: str | None = None
|
||||||
account_number: str | None = None
|
|
||||||
card_number: str | None = None
|
|
||||||
inn: str | None = None
|
inn: str | None = None
|
||||||
|
erc20: str | None = None
|
||||||
|
|
||||||
kyc_verified: bool | None = None
|
kyc_verified: bool | None = None
|
||||||
is_deleted: bool | None = None
|
is_deleted: bool | None = None
|
||||||
|
|||||||
5
src/infrastructure/cache/keydb_client.py
vendored
5
src/infrastructure/cache/keydb_client.py
vendored
@@ -39,10 +39,9 @@ class KeydbCache(ICache):
|
|||||||
'birth_date': str(user.birth_date) if user.birth_date else None,
|
'birth_date': str(user.birth_date) if user.birth_date else None,
|
||||||
'crypto_wallet': user.crypto_wallet,
|
'crypto_wallet': user.crypto_wallet,
|
||||||
'phone': user.phone,
|
'phone': user.phone,
|
||||||
'bik': user.bik,
|
'passport_data': user.passport_data,
|
||||||
'account_number': user.account_number,
|
|
||||||
'card_number': user.card_number,
|
|
||||||
'inn': user.inn,
|
'inn': user.inn,
|
||||||
|
'erc20': user.erc20,
|
||||||
'kyc_verified': user.kyc_verified,
|
'kyc_verified': user.kyc_verified,
|
||||||
'is_deleted': user.is_deleted,
|
'is_deleted': user.is_deleted,
|
||||||
'created_at': user.created_at.isoformat() if user.created_at else None,
|
'created_at': user.created_at.isoformat() if user.created_at else None,
|
||||||
|
|||||||
@@ -42,10 +42,9 @@ class UserRepository(IUserRepository):
|
|||||||
birth_date=user.birth_date,
|
birth_date=user.birth_date,
|
||||||
crypto_wallet=user.crypto_wallet,
|
crypto_wallet=user.crypto_wallet,
|
||||||
phone=user.phone,
|
phone=user.phone,
|
||||||
bik=user.bik,
|
passport_data=user.passport_data,
|
||||||
account_number=user.account_number,
|
|
||||||
card_number=user.card_number,
|
|
||||||
inn=user.inn,
|
inn=user.inn,
|
||||||
|
erc20=user.erc20,
|
||||||
kyc_verified_at=user.kyc_verified_at,
|
kyc_verified_at=user.kyc_verified_at,
|
||||||
kyc_verified=user.kyc_verified,
|
kyc_verified=user.kyc_verified,
|
||||||
is_deleted=user.is_deleted,
|
is_deleted=user.is_deleted,
|
||||||
@@ -81,7 +80,14 @@ class UserRepository(IUserRepository):
|
|||||||
return await self._update_field(user_id, phone=phone)
|
return await self._update_field(user_id, phone=phone)
|
||||||
|
|
||||||
async def set_bank_details(self, user_id: str, **fields: str) -> UserEntity:
|
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:
|
async def set_crypto_wallet(self, user_id: str, wallet_address: str) -> UserEntity:
|
||||||
return await self._update_field(user_id, crypto_wallet=wallet_address)
|
return await self._update_field(user_id, crypto_wallet=wallet_address)
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ async def me(
|
|||||||
'birth_date': str(user.birth_date) if user.birth_date else None,
|
'birth_date': str(user.birth_date) if user.birth_date else None,
|
||||||
'crypto_wallet': user.crypto_wallet,
|
'crypto_wallet': user.crypto_wallet,
|
||||||
'phone': user.phone,
|
'phone': user.phone,
|
||||||
'bik': user.bik,
|
'passport_data': user.passport_data,
|
||||||
'account_number': user.account_number,
|
|
||||||
'card_number': user.card_number,
|
|
||||||
'inn': user.inn,
|
'inn': user.inn,
|
||||||
|
'erc20': user.erc20,
|
||||||
'kyc_verified': user.kyc_verified,
|
'kyc_verified': user.kyc_verified,
|
||||||
'is_deleted': user.is_deleted,
|
'is_deleted': user.is_deleted,
|
||||||
'created_at': user.created_at.isoformat() if user.created_at else None,
|
'created_at': user.created_at.isoformat() if user.created_at else None,
|
||||||
|
|||||||
@@ -136,15 +136,15 @@ async def bank_details_complete(
|
|||||||
user = await command(
|
user = await command(
|
||||||
user_id=auth.user_id,
|
user_id=auth.user_id,
|
||||||
code=body.code,
|
code=body.code,
|
||||||
bik=body.bik,
|
passport_data=body.passport_data,
|
||||||
account_number=body.account_number,
|
inn=body.inn,
|
||||||
card_number=body.card_number,
|
erc20=body.erc20,
|
||||||
)
|
)
|
||||||
return ORJSONResponse(
|
return ORJSONResponse(
|
||||||
status_code=status.HTTP_200_OK,
|
status_code=status.HTTP_200_OK,
|
||||||
content={
|
content={
|
||||||
'bik': user.bik,
|
'passport_data': user.passport_data,
|
||||||
'account_number': user.account_number,
|
'inn': user.inn,
|
||||||
'card_number': user.card_number,
|
'erc20': user.erc20,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,69 +4,67 @@ from pydantic import BaseModel, field_validator, model_validator
|
|||||||
|
|
||||||
|
|
||||||
class BankUpdateRequest(BaseModel):
|
class BankUpdateRequest(BaseModel):
|
||||||
bik: str | None = None
|
passport_data: str | None = None
|
||||||
account_number: str | None = None
|
inn: str | None = None
|
||||||
card_number: str | None = None
|
erc20: str | None = None
|
||||||
|
|
||||||
@model_validator(mode='after')
|
@model_validator(mode='after')
|
||||||
def at_least_one(self) -> Self:
|
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')
|
raise ValueError('At least one field is required')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@field_validator('bik')
|
@field_validator('passport_data', 'inn', 'erc20')
|
||||||
@classmethod
|
@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:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
if not re.match(r'^\d{9}$', v):
|
if not re.match(r'^\d{10}(\d{2})?$', v):
|
||||||
raise ValueError('BIK must be exactly 9 digits')
|
raise ValueError('INN must be 10 or 12 digits')
|
||||||
|
if len(v) > 12:
|
||||||
|
raise ValueError('INN is too long')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator('account_number')
|
@field_validator('erc20')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_account_number(cls, v: str | None) -> str | None:
|
def validate_erc20(cls, v: str | None) -> str | None:
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
if not re.match(r'^\d{20}$', v):
|
if not re.match(r'^0x[a-fA-F0-9]{40}$', v):
|
||||||
raise ValueError('Account number must be exactly 20 digits')
|
raise ValueError('ERC20 address must be 0x followed by 40 hex characters')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator('card_number')
|
@field_validator('passport_data')
|
||||||
@classmethod
|
@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:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
v = re.sub(r'[\s\-]', '', v)
|
v = v.strip()
|
||||||
if not re.match(r'^\d{13,19}$', v):
|
if len(v) > 255:
|
||||||
raise ValueError('Card number must be 13-19 digits')
|
raise ValueError('Passport data is too long')
|
||||||
if not cls._luhn_check(v):
|
|
||||||
raise ValueError('Invalid card number (Luhn check failed)')
|
|
||||||
return v
|
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):
|
class BankConfirmRequest(BaseModel):
|
||||||
code: str
|
code: str
|
||||||
bik: str | None = None
|
passport_data: str | None = None
|
||||||
account_number: str | None = None
|
inn: str | None = None
|
||||||
card_number: str | None = None
|
erc20: str | None = None
|
||||||
|
|
||||||
@model_validator(mode='after')
|
@model_validator(mode='after')
|
||||||
def at_least_one_field(self) -> Self:
|
def at_least_one_field(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 bank field is required')
|
raise ValueError('At least one field is required')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@field_validator('code')
|
@field_validator('code')
|
||||||
@@ -77,34 +75,42 @@ class BankConfirmRequest(BaseModel):
|
|||||||
raise ValueError('Code must be exactly 6 digits')
|
raise ValueError('Code must be exactly 6 digits')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator('bik')
|
@field_validator('passport_data', 'inn', 'erc20')
|
||||||
@classmethod
|
@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:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
if not re.match(r'^\d{9}$', v):
|
if not re.match(r'^\d{10}(\d{2})?$', v):
|
||||||
raise ValueError('BIK must be exactly 9 digits')
|
raise ValueError('INN must be 10 or 12 digits')
|
||||||
|
if len(v) > 12:
|
||||||
|
raise ValueError('INN is too long')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator('account_number')
|
@field_validator('erc20')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_account_number(cls, v: str | None) -> str | None:
|
def validate_erc20(cls, v: str | None) -> str | None:
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
if not re.match(r'^\d{20}$', v):
|
if not re.match(r'^0x[a-fA-F0-9]{40}$', v):
|
||||||
raise ValueError('Account number must be exactly 20 digits')
|
raise ValueError('ERC20 address must be 0x followed by 40 hex characters')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator('card_number')
|
@field_validator('passport_data')
|
||||||
@classmethod
|
@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:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
v = re.sub(r'[\s\-]', '', v)
|
v = v.strip()
|
||||||
if not re.match(r'^\d{13,19}$', v):
|
if len(v) > 255:
|
||||||
raise ValueError('Card number must be 13-19 digits')
|
raise ValueError('Passport data is too long')
|
||||||
if not BankUpdateRequest._luhn_check(v):
|
|
||||||
raise ValueError('Invalid card number (Luhn check failed)')
|
|
||||||
return v
|
return v
|
||||||
|
|||||||
Reference in New Issue
Block a user