'decimal:4', 'processing_cost' => 'decimal:4', 'loss_rate' => 'decimal:2', 'margin_rate' => 'decimal:2', 'sales_price' => 'decimal:4', 'rounding_unit' => 'integer', 'effective_from' => 'date', 'effective_to' => 'date', 'is_final' => 'boolean', 'finalized_at' => 'datetime', ]; /** * 고객 그룹 관계 */ public function clientGroup(): BelongsTo { return $this->belongsTo(ClientGroup::class, 'client_group_id'); } /** * 리비전 이력 관계 */ public function revisions(): HasMany { return $this->hasMany(PriceRevision::class, 'price_id')->orderBy('revision_number', 'desc'); } /** * 품목 관계 (Polymorphic - item_type_code에 따라) */ public function item() { if ($this->item_type_code === 'PRODUCT') { return $this->belongsTo(Product::class, 'item_id'); } elseif ($this->item_type_code === 'MATERIAL') { return $this->belongsTo(Material::class, 'item_id'); } return null; } /** * 제품 관계 (item_type_code = PRODUCT인 경우) */ public function product(): BelongsTo { return $this->belongsTo(Product::class, 'item_id'); } /** * 자재 관계 (item_type_code = MATERIAL인 경우) */ public function material(): BelongsTo { return $this->belongsTo(Material::class, 'item_id'); } // ========== 스코프 ========== /** * 특정 품목 필터 */ public function scopeForItem($query, string $itemType, int $itemId) { return $query->where('item_type_code', $itemType) ->where('item_id', $itemId); } /** * 고객 그룹 필터 */ public function scopeForClientGroup($query, ?int $clientGroupId) { return $query->where('client_group_id', $clientGroupId); } /** * 특정 일자에 유효한 단가 */ public function scopeValidAt($query, $date) { return $query->where('effective_from', '<=', $date) ->where(function ($q) use ($date) { $q->whereNull('effective_to') ->orWhere('effective_to', '>=', $date); }); } /** * 상태 필터 */ public function scopeStatus($query, string $status) { return $query->where('status', $status); } /** * 활성 단가만 */ public function scopeActive($query) { return $query->where('status', 'active'); } /** * 확정된 단가만 */ public function scopeFinalized($query) { return $query->where('is_final', true); } // ========== 계산 메서드 ========== /** * 총원가 계산 * 총원가 = (매입단가 + 가공비) × (1 + LOSS율/100) */ public function calculateTotalCost(): float { $baseCost = ($this->purchase_price ?? 0) + ($this->processing_cost ?? 0); $lossMultiplier = 1 + (($this->loss_rate ?? 0) / 100); return $baseCost * $lossMultiplier; } /** * 판매단가 계산 (마진율 기반) * 판매단가 = 반올림(총원가 × (1 + 마진율/100), 반올림단위, 반올림규칙) */ public function calculateSalesPrice(): float { $totalCost = $this->calculateTotalCost(); $marginMultiplier = 1 + (($this->margin_rate ?? 0) / 100); $rawPrice = $totalCost * $marginMultiplier; return $this->applyRounding($rawPrice); } /** * 반올림 적용 */ private function applyRounding(float $value): float { $unit = $this->rounding_unit ?: 1; return match ($this->rounding_rule) { 'ceil' => ceil($value / $unit) * $unit, 'floor' => floor($value / $unit) * $unit, default => round($value / $unit) * $unit, // 'round' }; } /** * 확정 가능 여부 */ public function canFinalize(): bool { return ! $this->is_final && in_array($this->status, ['draft', 'active']); } /** * 수정 가능 여부 */ public function canEdit(): bool { return ! $this->is_final; } /** * 스냅샷 생성 (리비전용) */ public function toSnapshot(): array { return [ 'purchase_price' => $this->purchase_price, 'processing_cost' => $this->processing_cost, 'loss_rate' => $this->loss_rate, 'margin_rate' => $this->margin_rate, 'sales_price' => $this->sales_price, 'rounding_rule' => $this->rounding_rule, 'rounding_unit' => $this->rounding_unit, 'supplier' => $this->supplier, 'effective_from' => $this->effective_from?->format('Y-m-d'), 'effective_to' => $this->effective_to?->format('Y-m-d'), 'status' => $this->status, 'is_final' => $this->is_final, 'note' => $this->note, ]; } }