o2o-negosium-original/negodata/backend/crud/supplier_user_crud.py
Mina Choi 7f1c0be2b3 [feat] negodata: 협력사 채팅 계정(supplier_users) 관리 — 발급·비번 재설정·활성상태
- 백엔드: supplier_users/supplier_user_tokens ORM 매핑, /v1/supplier/{id}/account 4종(조회·발급·재설정·상태), 회사 스코프 게이팅, 재설정·비활성 시 토큰 삭제로 기존 로그인 즉시 무효화
- 비밀번호: 발급=서버 자동생성(1회 반환), 재설정=커스텀 지정 가능(미지정 시 자동생성)
- 프론트: 협력사 상세 SupplierAccountManager 섹션 + 목록 '채팅 계정' 컬럼, orval 재생성(SupplierType 은 enumLabels 로컬 상수로 이동)
- 테스트: test_supplier_account.py 9건, 전체 스위트 green
2026-07-10 10:14:00 +09:00

123 lines
5.3 KiB
Python

from abc import ABC, abstractmethod
from typing import Optional, Tuple
from sqlalchemy import select, update, delete
from sqlalchemy.ext.asyncio import AsyncSession
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import supplier_users, supplier_user_tokens
from common.enums import ErrorType
from common.logger import LOG
from common.utils.gtime import GTime
# 채팅(협상) 로그인 계정 CRUD. supplier.supplier_users 에는 company_id 가 없으므로
# 회사 스코프(협력사 소유권) 확인은 호출측(서비스)이 마친 뒤 supplier_id 로만 접근한다.
class ISupplierUserCRUD(ABC):
@abstractmethod
async def get_by_supplier(self, cdb: AsyncSession, supplier_id) -> Tuple[ErrorType, Optional[supplier_users]]:
pass
@abstractmethod
async def login_id_exists(self, cdb: AsyncSession, login_id: str) -> Tuple[ErrorType, bool]:
pass
@abstractmethod
async def account_map(self, cdb: AsyncSession, supplier_ids) -> Tuple[ErrorType, dict]:
pass
@abstractmethod
async def add_account(self, cdb: AsyncSession, account: supplier_users) -> ErrorType:
pass
@abstractmethod
async def update_account(self, cdb: AsyncSession, su_id, data: dict) -> ErrorType:
pass
@abstractmethod
async def delete_tokens(self, cdb: AsyncSession, su_id) -> ErrorType:
pass
class SupplierUserCRUD(ISupplierUserCRUD):
async def get_by_supplier(self, cdb: AsyncSession, supplier_id) -> Tuple[ErrorType, Optional[supplier_users]]:
"""협력사의 대표 계정 1건(발급순 첫 계정). 미발급이면 (SUCCESS, None) — 에러가 아니다."""
try:
query = (
select(supplier_users)
.where(supplier_users.supplier_id == supplier_id, supplier_users.deleted == False) # noqa: E712
.order_by(supplier_users.created_at.asc())
.limit(1)
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, None
return ErrorType.SUCCESS, rows[0] if rows else None
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None
async def login_id_exists(self, cdb: AsyncSession, login_id: str) -> Tuple[ErrorType, bool]:
"""로그인 ID 는 채팅 로그인 전역 유일(회사 스코프 아님) — 루트 backend 계정생성과 같은 기준."""
try:
query = select(supplier_users.su_id).where(
supplier_users.id == login_id,
supplier_users.deleted == False, # noqa: E712
).limit(1)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, False
return ErrorType.SUCCESS, len(rows) > 0
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, False
async def account_map(self, cdb: AsyncSession, supplier_ids) -> Tuple[ErrorType, dict]:
"""supplier_id 목록 → {supplier_id: (login_id, status)}. 목록/상세의 채팅 계정 표기용 배치 조인.
협력사당 여러 계정(시드 등)이 있으면 발급순 첫 계정만 대표로 남긴다."""
try:
if not supplier_ids:
return ErrorType.SUCCESS, {}
query = (
select(supplier_users.supplier_id, supplier_users.id, supplier_users.status)
.where(supplier_users.supplier_id.in_(supplier_ids), supplier_users.deleted == False) # noqa: E712
.order_by(supplier_users.created_at.asc())
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, {}
result = {}
for sid, login_id, status in rows:
if sid not in result:
result[sid] = (login_id, status)
return ErrorType.SUCCESS, result
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, {}
async def add_account(self, cdb: AsyncSession, account: supplier_users) -> ErrorType:
try:
return await DB_SESSION_MNG.insert(cdb, account)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def update_account(self, cdb: AsyncSession, su_id, data: dict) -> ErrorType:
try:
if not data:
return ErrorType.SUCCESS
query = update(supplier_users).where(supplier_users.su_id == su_id).values(**data, updated_at=GTime.UTC())
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def delete_tokens(self, cdb: AsyncSession, su_id) -> ErrorType:
"""하드 삭제 — 루트 backend 가 로그인/로그아웃 때 하드 삭제하는 것과 같은 방식."""
try:
query = delete(supplier_user_tokens).where(supplier_user_tokens.su_id == su_id)
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED