Files
sam-api/app/Services/Estimate/EstimateService.php
hskwon a6b06be61d feat: 견적 단가 자동 적용 기능 추가
- 고객 그룹별 단가 조정 지원
- 견적 생성 시 자동 단가 조회
- 매출단가만 사용 (매입단가는 경고)
2025-10-13 21:52:34 +09:00

400 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services\Estimate;
use App\Models\Estimate\Estimate;
use App\Models\Estimate\EstimateItem;
use App\Services\Calculation\CalculationEngine;
use App\Services\ModelSet\ModelSetService;
use App\Services\Pricing\PricingService;
use App\Services\Service;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class EstimateService extends Service
{
protected ModelSetService $modelSetService;
protected CalculationEngine $calculationEngine;
protected PricingService $pricingService;
public function __construct(
ModelSetService $modelSetService,
CalculationEngine $calculationEngine,
PricingService $pricingService
) {
parent::__construct();
$this->modelSetService = $modelSetService;
$this->calculationEngine = $calculationEngine;
$this->pricingService = $pricingService;
}
/**
* 견적 목록 조회
*/
public function getEstimates(array $filters = []): LengthAwarePaginator
{
$query = Estimate::with(['modelSet', 'items'])
->where('tenant_id', $this->tenantId());
// 필터링
if (! empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (! empty($filters['customer_name'])) {
$query->where('customer_name', 'like', '%'.$filters['customer_name'].'%');
}
if (! empty($filters['model_set_id'])) {
$query->where('model_set_id', $filters['model_set_id']);
}
if (! empty($filters['date_from'])) {
$query->whereDate('created_at', '>=', $filters['date_from']);
}
if (! empty($filters['date_to'])) {
$query->whereDate('created_at', '<=', $filters['date_to']);
}
if (! empty($filters['search'])) {
$searchTerm = $filters['search'];
$query->where(function ($q) use ($searchTerm) {
$q->where('estimate_name', 'like', '%'.$searchTerm.'%')
->orWhere('estimate_no', 'like', '%'.$searchTerm.'%')
->orWhere('project_name', 'like', '%'.$searchTerm.'%');
});
}
return $query->orderBy('created_at', 'desc')
->paginate($filters['per_page'] ?? 20);
}
/**
* 견적 상세 조회
*/
public function getEstimateDetail($estimateId): array
{
$estimate = Estimate::with(['modelSet.fields', 'items'])
->where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
return [
'estimate' => $estimate,
'model_set_schema' => $this->modelSetService->getModelSetCategoryFields($estimate->model_set_id),
'calculation_summary' => $this->summarizeCalculations($estimate),
];
}
/**
* 견적 생성
*/
public function createEstimate(array $data): array
{
return DB::transaction(function () use ($data) {
// 견적번호 생성
$estimateNo = Estimate::generateEstimateNo($this->tenantId());
// 모델셋 기반 BOM 계산
$bomCalculation = $this->modelSetService->calculateModelSetBom(
$data['model_set_id'],
$data['parameters']
);
// 견적 생성
$estimate = Estimate::create([
'tenant_id' => $this->tenantId(),
'model_set_id' => $data['model_set_id'],
'estimate_no' => $estimateNo,
'estimate_name' => $data['estimate_name'],
'customer_name' => $data['customer_name'] ?? null,
'project_name' => $data['project_name'] ?? null,
'parameters' => $data['parameters'],
'calculated_results' => $bomCalculation['calculated_values'] ?? [],
'bom_data' => $bomCalculation,
'total_amount' => 0, // 항목 생성 후 재계산
'notes' => $data['notes'] ?? null,
'valid_until' => now()->addDays(30), // 기본 30일 유효
'created_by' => $this->apiUserId(),
]);
// 견적 항목 생성 (BOM 기반) + 가격 계산
if (! empty($bomCalculation['bom_items'])) {
$totalAmount = $this->createEstimateItems(
$estimate,
$bomCalculation['bom_items'],
$data['client_id'] ?? null
);
// 총액 업데이트
$estimate->update(['total_amount' => $totalAmount]);
}
return $this->getEstimateDetail($estimate->id);
});
}
/**
* 견적 수정
*/
public function updateEstimate($estimateId, array $data): array
{
return DB::transaction(function () use ($estimateId, $data) {
$estimate = Estimate::where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
// 파라미터가 변경되면 재계산
if (isset($data['parameters'])) {
$bomCalculation = $this->modelSetService->calculateModelSetBom(
$estimate->model_set_id,
$data['parameters']
);
$data['calculated_results'] = $bomCalculation['calculated_values'] ?? [];
$data['bom_data'] = $bomCalculation;
// 기존 견적 항목 삭제 후 재생성
$estimate->items()->delete();
if (! empty($bomCalculation['bom_items'])) {
$totalAmount = $this->createEstimateItems(
$estimate,
$bomCalculation['bom_items'],
$data['client_id'] ?? null
);
$data['total_amount'] = $totalAmount;
}
}
$estimate->update([
...$data,
'updated_by' => $this->apiUserId(),
]);
return $this->getEstimateDetail($estimate->id);
});
}
/**
* 견적 삭제
*/
public function deleteEstimate($estimateId): void
{
DB::transaction(function () use ($estimateId) {
$estimate = Estimate::where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
// 진행 중인 견적은 삭제 불가
if (in_array($estimate->status, ['SENT', 'APPROVED'])) {
throw new \Exception(__('error.estimate.cannot_delete_sent_or_approved'));
}
$estimate->update(['deleted_by' => $this->apiUserId()]);
$estimate->delete();
});
}
/**
* 견적 복제
*/
public function cloneEstimate($estimateId, array $data): array
{
return DB::transaction(function () use ($estimateId, $data) {
$originalEstimate = Estimate::with('items')
->where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
// 새 견적번호 생성
$newEstimateNo = Estimate::generateEstimateNo($this->tenantId());
// 견적 복제
$newEstimate = Estimate::create([
'tenant_id' => $this->tenantId(),
'model_set_id' => $originalEstimate->model_set_id,
'estimate_no' => $newEstimateNo,
'estimate_name' => $data['estimate_name'],
'customer_name' => $data['customer_name'] ?? $originalEstimate->customer_name,
'project_name' => $data['project_name'] ?? $originalEstimate->project_name,
'parameters' => $originalEstimate->parameters,
'calculated_results' => $originalEstimate->calculated_results,
'bom_data' => $originalEstimate->bom_data,
'total_amount' => $originalEstimate->total_amount,
'notes' => $data['notes'] ?? $originalEstimate->notes,
'valid_until' => now()->addDays(30),
'created_by' => $this->apiUserId(),
]);
// 견적 항목 복제
foreach ($originalEstimate->items as $item) {
EstimateItem::create([
'tenant_id' => $this->tenantId(),
'estimate_id' => $newEstimate->id,
'sequence' => $item->sequence,
'item_name' => $item->item_name,
'item_description' => $item->item_description,
'parameters' => $item->parameters,
'calculated_values' => $item->calculated_values,
'unit_price' => $item->unit_price,
'quantity' => $item->quantity,
'total_price' => $item->total_price,
'bom_components' => $item->bom_components,
'notes' => $item->notes,
'created_by' => $this->apiUserId(),
]);
}
return $this->getEstimateDetail($newEstimate->id);
});
}
/**
* 견적 상태 변경
*/
public function changeEstimateStatus($estimateId, string $status, ?string $notes = null): array
{
$estimate = Estimate::where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
$validTransitions = [
'DRAFT' => ['SENT', 'REJECTED'],
'SENT' => ['APPROVED', 'REJECTED', 'EXPIRED'],
'APPROVED' => ['EXPIRED'],
'REJECTED' => ['DRAFT'],
'EXPIRED' => ['DRAFT'],
];
if (! in_array($status, $validTransitions[$estimate->status] ?? [])) {
throw new \Exception(__('error.estimate.invalid_status_transition'));
}
$estimate->update([
'status' => $status,
'notes' => $notes ? ($estimate->notes."\n\n".$notes) : $estimate->notes,
'updated_by' => $this->apiUserId(),
]);
return $this->getEstimateDetail($estimate->id);
}
/**
* 동적 견적 폼 스키마 조회
*/
public function getEstimateFormSchema($modelSetId): array
{
$parameters = $this->modelSetService->getEstimateParameters($modelSetId);
return [
'model_set' => $parameters['category'],
'form_schema' => [
'input_fields' => $parameters['input_fields'],
'calculated_fields' => $parameters['calculated_fields'],
],
'calculation_schema' => $parameters['calculation_schema'],
];
}
/**
* 견적 파라미터 미리보기 계산
*/
public function previewCalculation($modelSetId, array $parameters): array
{
return $this->modelSetService->calculateModelSetBom($modelSetId, $parameters);
}
/**
* 견적 항목 생성 (가격 계산 포함)
*
* @return float 총 견적 금액
*/
protected function createEstimateItems(Estimate $estimate, array $bomItems, ?int $clientId = null): float
{
$totalAmount = 0;
$warnings = [];
foreach ($bomItems as $index => $bomItem) {
$quantity = $bomItem['quantity'] ?? 1;
$unitPrice = 0;
// 가격 조회 (item_id와 item_type이 있는 경우)
if (isset($bomItem['item_id']) && isset($bomItem['item_type'])) {
$priceResult = $this->pricingService->getItemPrice(
$bomItem['item_type'], // 'PRODUCT' or 'MATERIAL'
$bomItem['item_id'],
$clientId,
now()->format('Y-m-d')
);
$unitPrice = $priceResult['price'] ?? 0;
if ($priceResult['warning']) {
$warnings[] = $priceResult['warning'];
}
}
$totalPrice = $unitPrice * $quantity;
$totalAmount += $totalPrice;
EstimateItem::create([
'tenant_id' => $this->tenantId(),
'estimate_id' => $estimate->id,
'sequence' => $index + 1,
'item_name' => $bomItem['name'] ?? '견적 항목 '.($index + 1),
'item_description' => $bomItem['description'] ?? '',
'parameters' => $bomItem['parameters'] ?? [],
'calculated_values' => $bomItem['calculated_values'] ?? [],
'unit_price' => $unitPrice,
'quantity' => $quantity,
'total_price' => $totalPrice,
'bom_components' => $bomItem['components'] ?? [],
'created_by' => $this->apiUserId(),
]);
}
// 가격 경고가 있으면 로그 기록
if (! empty($warnings)) {
\Log::warning('견적 가격 조회 경고', [
'estimate_id' => $estimate->id,
'warnings' => $warnings,
]);
}
return $totalAmount;
}
/**
* 계산 결과 요약
*/
protected function summarizeCalculations(Estimate $estimate): array
{
$summary = [
'total_items' => $estimate->items->count(),
'total_amount' => $estimate->total_amount,
'key_calculations' => [],
];
// 주요 계산 결과 추출
if (! empty($estimate->calculated_results)) {
$results = $estimate->calculated_results;
if (isset($results['W1'], $results['H1'])) {
$summary['key_calculations']['제작사이즈'] = $results['W1'].' × '.$results['H1'].' mm';
}
if (isset($results['weight'])) {
$summary['key_calculations']['중량'] = $results['weight'].' kg';
}
if (isset($results['area'])) {
$summary['key_calculations']['면적'] = $results['area'].' ㎡';
}
if (isset($results['bracket_size'])) {
$summary['key_calculations']['모터브라켓'] = $results['bracket_size'];
}
}
return $summary;
}
}