feat: Price, PriceRevision 모델 생성 및 서비스 정리
- Price 모델 생성 (prices 테이블) - PriceRevision 모델 생성 (price_revisions 테이블) - 레거시 Pricing/PricingService 삭제 (PriceHistory 사용) - pricing 에러/메시지 추가
This commit is contained in:
221
app/Models/Products/Price.php
Normal file
221
app/Models/Products/Price.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\Orders\ClientGroup;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Price extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'prices';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'item_type_code',
|
||||
'item_id',
|
||||
'client_group_id',
|
||||
'purchase_price',
|
||||
'processing_cost',
|
||||
'loss_rate',
|
||||
'margin_rate',
|
||||
'sales_price',
|
||||
'rounding_rule',
|
||||
'rounding_unit',
|
||||
'supplier',
|
||||
'effective_from',
|
||||
'effective_to',
|
||||
'note',
|
||||
'status',
|
||||
'is_final',
|
||||
'finalized_at',
|
||||
'finalized_by',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'item_id' => '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,
|
||||
];
|
||||
}
|
||||
}
|
||||
60
app/Models/Products/PriceRevision.php
Normal file
60
app/Models/Products/PriceRevision.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PriceRevision extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'price_revisions';
|
||||
|
||||
/**
|
||||
* created_at만 사용 (updated_at 없음)
|
||||
*/
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'price_id',
|
||||
'revision_number',
|
||||
'changed_at',
|
||||
'changed_by',
|
||||
'change_reason',
|
||||
'before_snapshot',
|
||||
'after_snapshot',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price_id' => 'integer',
|
||||
'revision_number' => 'integer',
|
||||
'changed_at' => 'datetime',
|
||||
'changed_by' => 'integer',
|
||||
'before_snapshot' => 'array',
|
||||
'after_snapshot' => 'array',
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Relations
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 단가 관계
|
||||
*/
|
||||
public function price(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Price::class, 'price_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 변경자 관계
|
||||
*/
|
||||
public function changedByUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'changed_by');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user