268 lines
11 KiB
Python
268 lines
11 KiB
Python
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
|