import uuid from fastapi import Depends from common.database.db_session_manager import DB_SESSION_MNG from common.database.model.models import supplier_items from common.enums import DBWRType, ErrorType, SupplierType from common.models.gmodel import Res_WebPacketProtocol from crud.item_crud import IItemCRUD, ItemCRUD from crud.supplier_crud import ISupplierCRUD, SupplierCRUD from crud.supplier_item_crud import ISupplierItemCRUD, SupplierItemCRUD from router.v1.supplier_item.protocol import ( ItemSupplyType, Req_CreateSupplierItem, Req_UpdateSupplyType, Res_BulkMapByNames, Res_ItemSupplyTypeList, Res_SupplierItem, Res_SupplierItemList, SupplierItemData, ) _VALID_SUPPLY_TYPES = {e.value for e in SupplierType} class SupplierItemService: """협력사-상품 매핑 로직. 소유권은 상위 협력사/상품의 company_id 로 확인한다(멀티테넌트).""" def __init__( self, supplier_item_crud: ISupplierItemCRUD = Depends(SupplierItemCRUD), supplier_crud: ISupplierCRUD = Depends(SupplierCRUD), item_crud: IItemCRUD = Depends(ItemCRUD), ): self.supplier_item_crud = supplier_item_crud self.supplier_crud = supplier_crud self.item_crud = item_crud async def _supplier_owned(self, company_uuid: uuid.UUID, supplier_id: uuid.UUID) -> ErrorType: err_type, supplier = await DB_SESSION_MNG.execute_lambda( supplier_items.DBType(), DBWRType.DB_READ.value, lambda s: self.supplier_crud.get_by_id(s, supplier_id), ) if err_type != ErrorType.SUCCESS or supplier is None or supplier.company_id != company_uuid: return ErrorType.SUPPLIER_NOT_FOUND return ErrorType.SUCCESS async def _item_owned(self, company_uuid: uuid.UUID, item_id: uuid.UUID): err_type, item = await DB_SESSION_MNG.execute_lambda( supplier_items.DBType(), DBWRType.DB_READ.value, lambda s: self.item_crud.get_by_id(s, item_id), ) if err_type != ErrorType.SUCCESS or item is None or item.company_id != company_uuid: return ErrorType.ITEM_NOT_FOUND, None return ErrorType.SUCCESS, item async def _fetch_owned_mapping(self, company_uuid: uuid.UUID, supplier_item_id: uuid.UUID): """매핑 조회 + 소유 협력사 확인. (ErrorType, mapping|None) 반환.""" err_type, mapping = await DB_SESSION_MNG.execute_lambda( supplier_items.DBType(), DBWRType.DB_READ.value, lambda s: self.supplier_item_crud.get_by_id(s, supplier_item_id), ) if err_type != ErrorType.SUCCESS or mapping is None: return ErrorType.SUPPLIER_ITEM_NOT_FOUND, None own_err = await self._supplier_owned(company_uuid, mapping.supplier_id) if own_err != ErrorType.SUCCESS: return ErrorType.SUPPLIER_ITEM_NOT_FOUND, None return ErrorType.SUCCESS, mapping async def list_by_supplier(self, company_id: str, supplier_id: str) -> Res_SupplierItemList: res = Res_SupplierItemList() own_err = await self._supplier_owned(uuid.UUID(company_id), uuid.UUID(supplier_id)) if own_err != ErrorType.SUCCESS: res.result.SetResult(own_err) return res err_type, rows = await DB_SESSION_MNG.execute_lambda( supplier_items.DBType(), DBWRType.DB_READ.value, lambda s: self.supplier_item_crud.list_by_supplier(s, uuid.UUID(supplier_id)), ) if err_type != ErrorType.SUCCESS: res.result.SetResult(err_type) return res # Row(supplier_item_id, item_id, name, code, supply_type, category, manufacturer) res.supplier_items = [ SupplierItemData( supplier_item_id=r[0], item_id=r[1], item_name=r[2], item_code=r[3], supply_type=r[4], item_category=r[5], item_manufacturer=r[6], ) for r in rows ] return res async def list_by_item(self, company_id: str, item_id: str) -> Res_ItemSupplyTypeList: res = Res_ItemSupplyTypeList() own_err, _ = await self._item_owned(uuid.UUID(company_id), uuid.UUID(item_id)) if own_err != ErrorType.SUCCESS: res.result.SetResult(own_err) return res err_type, rows = await DB_SESSION_MNG.execute_lambda( supplier_items.DBType(), DBWRType.DB_READ.value, lambda s: self.supplier_item_crud.list_by_item(s, uuid.UUID(item_id)), ) if err_type != ErrorType.SUCCESS: res.result.SetResult(err_type) return res # Row(supplier_id, supply_type, name, supplier_item_id) res.suppliers = [ ItemSupplyType(supplier_id=r[0], supply_type=r[1], supplier_name=r[2], supplier_item_id=r[3]) for r in rows ] return res async def create(self, company_id: str, req: Req_CreateSupplierItem) -> Res_SupplierItem: res = Res_SupplierItem() company_uuid = uuid.UUID(company_id) if req.supply_type not in _VALID_SUPPLY_TYPES: res.result.SetResult(ErrorType.INVALID_REQUEST_DATA) return res own_err = await self._supplier_owned(company_uuid, req.supplier_id) if own_err != ErrorType.SUCCESS: res.result.SetResult(own_err) return res item_err, item = await self._item_owned(company_uuid, req.item_id) if item_err != ErrorType.SUCCESS: res.result.SetResult(item_err) return res # 활성 중복 매핑 차단(부분 유니크 인덱스와 이중 방어). dup_err, existing = await DB_SESSION_MNG.execute_lambda( supplier_items.DBType(), DBWRType.DB_READ.value, lambda s: self.supplier_item_crud.existing_item_ids(s, req.supplier_id, [req.item_id]), ) if dup_err != ErrorType.SUCCESS: res.result.SetResult(dup_err) return res if existing: res.result.SetResult(ErrorType.DB_ALREADY_SAME_KEY) return res mapping = supplier_items(supplier_id=req.supplier_id, item_id=req.item_id, supply_type=req.supply_type) err_type = await DB_SESSION_MNG.execute_lambda_run( [supplier_items.DBType()], [lambda s: self.supplier_item_crud.add_many(s, [mapping])], ) if err_type != ErrorType.SUCCESS: res.result.SetResult(err_type) return res res.supplier_item = SupplierItemData( supplier_item_id=mapping.supplier_item_id, item_id=item.item_id, item_name=item.name, item_code=item.code, supply_type=req.supply_type, ) return res async def update_type(self, company_id: str, supplier_item_id: str, req: Req_UpdateSupplyType) -> Res_WebPacketProtocol: res = Res_WebPacketProtocol() if req.supply_type not in _VALID_SUPPLY_TYPES: res.result.SetResult(ErrorType.INVALID_REQUEST_DATA) return res err_type, mapping = await self._fetch_owned_mapping(uuid.UUID(company_id), uuid.UUID(supplier_item_id)) if err_type != ErrorType.SUCCESS: res.result.SetResult(err_type) return res err_type = await DB_SESSION_MNG.execute_lambda_run( [supplier_items.DBType()], [lambda s: self.supplier_item_crud.update_type(s, mapping.supplier_item_id, req.supply_type)], ) if err_type != ErrorType.SUCCESS: res.result.SetResult(err_type) return res async def delete(self, company_id: str, supplier_item_id: str) -> Res_WebPacketProtocol: res = Res_WebPacketProtocol() err_type, mapping = await self._fetch_owned_mapping(uuid.UUID(company_id), uuid.UUID(supplier_item_id)) if err_type != ErrorType.SUCCESS: res.result.SetResult(err_type) return res err_type = await DB_SESSION_MNG.execute_lambda_run( [supplier_items.DBType()], [lambda s: self.supplier_item_crud.soft_delete(s, mapping.supplier_item_id)], ) if err_type != ErrorType.SUCCESS: res.result.SetResult(err_type) return res async def bulk_map_by_names(self, company_id: str, supplier_id: str, names: list) -> Res_BulkMapByNames: """엑셀 취급상품 업로드용: 상품명 리스트 → 매핑 생성. 미매칭명은 스킵 후 리포트, 이미 매핑된 상품도 스킵. 업로드는 공급유형을 받지 않으므로 전부 없음(0)으로 들어간다(상세에서 편집).""" res = Res_BulkMapByNames() company_uuid = uuid.UUID(company_id) supplier_uuid = uuid.UUID(supplier_id) own_err = await self._supplier_owned(company_uuid, supplier_uuid) if own_err != ErrorType.SUCCESS: res.result.SetResult(own_err) return res # 입력 정규화: 공백 제거·빈값 제거·중복 제거(원문 순서 유지). seen = set() clean_names = [] for raw in names: n = (raw or "").strip() if n and n not in seen: seen.add(n) clean_names.append(n) if not clean_names: return res # 이름 → 상품 해석(정확 일치). 이름 중복 상품은 첫 매칭만 사용. find_err, rows = await DB_SESSION_MNG.execute_lambda( supplier_items.DBType(), DBWRType.DB_READ.value, lambda s: self.supplier_item_crud.find_items_by_names(s, company_uuid, clean_names), ) if find_err != ErrorType.SUCCESS: res.result.SetResult(find_err) return res name_to_item = {} for r in rows: # Row(item_id, name) if r[1] not in name_to_item: name_to_item[r[1]] = r[0] res.unmatched = [n for n in clean_names if n not in name_to_item] matched_item_ids = [name_to_item[n] for n in clean_names if n in name_to_item] if not matched_item_ids: return res # 이미 이 협력사에 매핑된 상품 제외. exist_err, already = await DB_SESSION_MNG.execute_lambda( supplier_items.DBType(), DBWRType.DB_READ.value, lambda s: self.supplier_item_crud.existing_item_ids(s, supplier_uuid, matched_item_ids), ) if exist_err != ErrorType.SUCCESS: res.result.SetResult(exist_err) return res already_set = set(already) to_create = [iid for iid in matched_item_ids if iid not in already_set] res.skipped_count = len(matched_item_ids) - len(to_create) if not to_create: return res mappings = [supplier_items(supplier_id=supplier_uuid, item_id=iid, supply_type=0) for iid in to_create] err_type = await DB_SESSION_MNG.execute_lambda_run( [supplier_items.DBType()], [lambda s: self.supplier_item_crud.add_many(s, mappings)], ) if err_type != ErrorType.SUCCESS: res.result.SetResult(err_type) return res res.created_count = len(to_create) return res