'integer', 'client_group_id' => 'integer', 'purchase_price' => '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', 'finalized_by' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'deleted_by' => 'integer', ]; // ───────────────────────────────────────────────────────────── // Relations // ───────────────────────────────────────────────────────────── /** * 고객 그룹 관계 */ public function clientGroup(): BelongsTo { return $this->belongsTo(ClientGroup::class, 'client_group_id'); } /** * 리비전 이력 */ public function revisions(): HasMany { return $this->hasMany(PriceRevision::class, 'price_id'); } // ───────────────────────────────────────────────────────────── // Scopes // ───────────────────────────────────────────────────────────── /** * 특정 품목 필터 */ 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, string $date) { return $query->where('effective_from', '<=', $date) ->where(function ($q) use ($date) { $q->whereNull('effective_to') ->orWhere('effective_to', '>=', $date); }); } /** * 활성 상태 단가 필터 */ public function scopeActive($query) { return $query->whereIn('status', ['active', 'finalized']); } // ───────────────────────────────────────────────────────────── // Business Logic // ───────────────────────────────────────────────────────────── /** * 수정 가능 여부 확인 */ public function canEdit(): bool { return ! $this->is_final; } /** * 확정 가능 여부 확인 */ public function canFinalize(): bool { if ($this->is_final) { return false; } // 필수 필드 확인 if (empty($this->sales_price) && empty($this->purchase_price)) { return false; } return true; } /** * 판매단가 자동 계산 * 공식: (매입단가 + 가공비) * (1 + LOSS율) * (1 + 마진율) */ public function calculateSalesPrice(): ?float { $purchasePrice = (float) ($this->purchase_price ?? 0); $processingCost = (float) ($this->processing_cost ?? 0); $lossRate = (float) ($this->loss_rate ?? 0) / 100; $marginRate = (float) ($this->margin_rate ?? 0) / 100; if ($purchasePrice <= 0) { return null; } $baseCost = $purchasePrice + $processingCost; $withLoss = $baseCost * (1 + $lossRate); $withMargin = $withLoss * (1 + $marginRate); // 반올림 적용 return $this->applyRounding($withMargin); } /** * 반올림 규칙 적용 */ private function applyRounding(float $value): float { $unit = $this->rounding_unit ?? 1; $rule = $this->rounding_rule ?? 'round'; if ($unit <= 0) { $unit = 1; } $result = match ($rule) { 'ceil' => ceil($value / $unit) * $unit, 'floor' => floor($value / $unit) * $unit, default => round($value / $unit) * $unit, }; return (float) $result; } /** * 스냅샷 데이터 생성 (리비전용) */ public function toSnapshot(): array { return [ 'item_type_code' => $this->item_type_code, 'item_id' => $this->item_id, 'client_group_id' => $this->client_group_id, '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?->toDateString(), 'effective_to' => $this->effective_to?->toDateString(), 'note' => $this->note, 'status' => $this->status, 'is_final' => $this->is_final, ]; } }