o2o-negosium-original/negodata/backend/services/supplier_item_service.py

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