Files
sam-api/app/Services/Pricing/PricingService.php
hskwon ddc4bb99a0 feat: 통합 품목 조회 API 및 가격 통합 시스템 구현
- 통합 품목 조회 API (materials + products UNION)
  - ItemsService, ItemsController, Swagger 문서 생성
  - 타입 필터링 (FG/PT/SM/RM/CS), 검색, 카테고리 지원
  - Collection merge 방식으로 UNION 쿼리 안정화

- 품목-가격 통합 조회
  - PricingService.getPriceByType() 추가 (SALE/PURCHASE 지원)
  - 단일 품목 조회 시 판매가/매입가 선택적 포함
  - 고객그룹 가격 우선순위 적용 및 시계열 조회

- 자재 타입 명시적 관리
  - materials.material_type 컬럼 추가 (SM/RM/CS)
  - 기존 데이터 344개 자동 변환 (RAW→RM, SUB→SM)
  - 인덱스 추가로 조회 성능 최적화

- DB 데이터 정규화
  - products.product_type: 760개 정규화 (PRODUCT→FG, PART/SUBASSEMBLY→PT)
  - 타입 코드 표준화로 API 일관성 확보

최종 데이터: 제품 760개(FG 297, PT 463), 자재 344개(SM 215, RM 129)
2025-11-11 11:30:17 +09:00

286 lines
8.8 KiB
PHP

<?php
namespace App\Services\Pricing;
use App\Models\Orders\Client;
use App\Models\Products\PriceHistory;
use App\Services\Service;
use Carbon\Carbon;
class PricingService extends Service
{
/**
* 특정 항목(제품/자재)의 단가를 조회
*
* @param string $itemType 'PRODUCT' | 'MATERIAL'
* @param int $itemId 제품/자재 ID
* @param int|null $clientId 고객 ID (NULL이면 기본 가격)
* @param string|null $date 기준일 (NULL이면 오늘)
* @return array ['price' => float|null, 'price_history_id' => int|null, 'client_group_id' => int|null, 'warning' => string|null]
*/
public function getItemPrice(string $itemType, int $itemId, ?int $clientId = null, ?string $date = null): array
{
$date = $date ?? Carbon::today()->format('Y-m-d');
$clientGroupId = null;
// 1. 고객의 그룹 ID 확인
if ($clientId) {
$client = Client::where('tenant_id', $this->tenantId())
->where('id', $clientId)
->first();
if ($client) {
$clientGroupId = $client->client_group_id;
}
}
// 2. 가격 조회 (우선순위대로)
$priceHistory = null;
// 1순위: 고객 그룹별 매출단가
if ($clientGroupId) {
$priceHistory = $this->findPrice($itemType, $itemId, $clientGroupId, $date);
}
// 2순위: 기본 매출단가 (client_group_id = NULL)
if (! $priceHistory) {
$priceHistory = $this->findPrice($itemType, $itemId, null, $date);
}
// 3순위: NULL (경고)
if (! $priceHistory) {
return [
'price' => null,
'price_history_id' => null,
'client_group_id' => null,
'warning' => __('error.price_not_found', [
'item_type' => $itemType,
'item_id' => $itemId,
'date' => $date,
]),
];
}
return [
'price' => (float) $priceHistory->price,
'price_history_id' => $priceHistory->id,
'client_group_id' => $priceHistory->client_group_id,
'warning' => null,
];
}
/**
* 특정 항목의 가격 타입별 단가 조회
*
* @param string $itemType 'PRODUCT' | 'MATERIAL'
* @param int $itemId 제품/자재 ID
* @param string $priceType 'SALE' | 'PURCHASE'
* @param int|null $clientId 고객 ID (NULL이면 기본 가격)
* @param string|null $date 기준일 (NULL이면 오늘)
* @return array ['price' => float|null, 'price_history_id' => int|null, 'client_group_id' => int|null, 'warning' => string|null]
*/
public function getPriceByType(
string $itemType,
int $itemId,
string $priceType,
?int $clientId = null,
?string $date = null
): array {
$date = $date ?? Carbon::today()->format('Y-m-d');
$clientGroupId = null;
// 1. 고객의 그룹 ID 확인
if ($clientId) {
$client = Client::where('tenant_id', $this->tenantId())
->where('id', $clientId)
->first();
if ($client) {
$clientGroupId = $client->client_group_id;
}
}
// 2. 가격 조회 (우선순위대로)
$priceHistory = null;
// 1순위: 고객 그룹별 가격
if ($clientGroupId) {
$priceHistory = $this->findPriceByType($itemType, $itemId, $priceType, $clientGroupId, $date);
}
// 2순위: 기본 가격 (client_group_id = NULL)
if (! $priceHistory) {
$priceHistory = $this->findPriceByType($itemType, $itemId, $priceType, null, $date);
}
// 3순위: NULL (경고)
if (! $priceHistory) {
return [
'price' => null,
'price_history_id' => null,
'client_group_id' => null,
'warning' => __('error.price_not_found', [
'item_type' => $itemType,
'item_id' => $itemId,
'price_type' => $priceType,
'date' => $date,
]),
];
}
return [
'price' => (float) $priceHistory->price,
'price_history_id' => $priceHistory->id,
'client_group_id' => $priceHistory->client_group_id,
'warning' => null,
];
}
/**
* 가격 이력에서 유효한 가격 조회 (SALE 타입 기본)
*/
private function findPrice(string $itemType, int $itemId, ?int $clientGroupId, string $date): ?PriceHistory
{
return $this->findPriceByType($itemType, $itemId, 'SALE', $clientGroupId, $date);
}
/**
* 가격 이력에서 유효한 가격 조회 (가격 타입 지정)
*
* @param string $priceType 'SALE' | 'PURCHASE'
*/
private function findPriceByType(
string $itemType,
int $itemId,
string $priceType,
?int $clientGroupId,
string $date
): ?PriceHistory {
$query = PriceHistory::where('tenant_id', $this->tenantId())
->forItem($itemType, $itemId)
->forClientGroup($clientGroupId);
// 가격 타입에 따라 스코프 적용
if ($priceType === 'PURCHASE') {
$query->purchasePrice();
} else {
$query->salePrice();
}
return $query->validAt($date)
->orderBy('started_at', 'desc')
->first();
}
/**
* 여러 항목의 단가를 일괄 조회
*
* @param array $items [['item_type' => 'PRODUCT', 'item_id' => 1], ...]
* @return array ['prices' => [...], 'warnings' => [...]]
*/
public function getBulkItemPrices(array $items, ?int $clientId = null, ?string $date = null): array
{
$prices = [];
$warnings = [];
foreach ($items as $item) {
$result = $this->getItemPrice(
$item['item_type'],
$item['item_id'],
$clientId,
$date
);
$prices[] = array_merge($item, [
'price' => $result['price'],
'price_history_id' => $result['price_history_id'],
'client_group_id' => $result['client_group_id'],
]);
if ($result['warning']) {
$warnings[] = $result['warning'];
}
}
return [
'prices' => $prices,
'warnings' => $warnings,
];
}
/**
* 가격 등록/수정
*/
public function upsertPrice(array $data): PriceHistory
{
$data['tenant_id'] = $this->tenantId();
$data['created_by'] = $this->apiUserId();
$data['updated_by'] = $this->apiUserId();
// 중복 확인: 동일 조건(item, client_group, date 범위)의 가격이 이미 있는지
$existing = PriceHistory::where('tenant_id', $data['tenant_id'])
->where('item_type_code', $data['item_type_code'])
->where('item_id', $data['item_id'])
->where('price_type_code', $data['price_type_code'])
->where('client_group_id', $data['client_group_id'] ?? null)
->where('started_at', $data['started_at'])
->first();
if ($existing) {
$existing->update($data);
return $existing->fresh();
}
return PriceHistory::create($data);
}
/**
* 가격 이력 조회 (페이지네이션)
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function listPrices(array $filters = [], int $perPage = 15)
{
$query = PriceHistory::where('tenant_id', $this->tenantId());
if (isset($filters['item_type_code'])) {
$query->where('item_type_code', $filters['item_type_code']);
}
if (isset($filters['item_id'])) {
$query->where('item_id', $filters['item_id']);
}
if (isset($filters['price_type_code'])) {
$query->where('price_type_code', $filters['price_type_code']);
}
if (isset($filters['client_group_id'])) {
$query->where('client_group_id', $filters['client_group_id']);
}
if (isset($filters['date'])) {
$query->validAt($filters['date']);
}
return $query->orderBy('started_at', 'desc')
->orderBy('created_at', 'desc')
->paginate($perPage);
}
/**
* 가격 삭제 (Soft Delete)
*/
public function deletePrice(int $id): bool
{
$price = PriceHistory::where('tenant_id', $this->tenantId())
->findOrFail($id);
$price->deleted_by = $this->apiUserId();
$price->save();
return $price->delete();
}
}