- numbering_rules 테이블: JSON 패턴 기반 채번 규칙 저장 (tenant별)
- numbering_sequences 테이블: MySQL UPSERT 기반 atomic 시퀀스 관리
- NumberingService: generate/preview/nextSequence 핵심 서비스
- QuoteNumberService: NumberingService 우선, 폴백 QT{YYYYMMDD}{NNNN}
- OrderService: NumberingService 우선 (pair_code 지원), 폴백 ORD{YYYYMMDD}{NNNN}
- StoreOrderRequest: pair_code 필드 추가
- NumberingRuleSeeder: tenant_id=287 견적(KD-PR)/수주(KD-{pairCode}) 규칙
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
156 lines
4.1 KiB
PHP
156 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Quote;
|
|
|
|
use App\Models\Quote\Quote;
|
|
use App\Services\NumberingService;
|
|
use App\Services\Service;
|
|
|
|
class QuoteNumberService extends Service
|
|
{
|
|
public function __construct(
|
|
private NumberingService $numberingService
|
|
) {}
|
|
|
|
/**
|
|
* 견적번호 생성
|
|
*
|
|
* 채번규칙이 있으면 NumberingService 사용, 없으면 레거시 로직
|
|
*/
|
|
public function generate(?string $productCategory = null): string
|
|
{
|
|
$this->numberingService->setContext(
|
|
$this->tenantId(),
|
|
$this->apiUserId()
|
|
);
|
|
|
|
$number = $this->numberingService->generate('quote', [
|
|
'product_category' => $productCategory,
|
|
]);
|
|
|
|
if ($number !== null) {
|
|
return $number;
|
|
}
|
|
|
|
return $this->generateLegacy($productCategory);
|
|
}
|
|
|
|
/**
|
|
* 기본 견적번호 생성
|
|
*
|
|
* 형식: QT{YYYYMMDD}{NNNN}
|
|
* 예시: QT202602070001
|
|
*/
|
|
private function generateLegacy(?string $productCategory = null): string
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$prefix = 'QT';
|
|
$date = now()->format('Ymd');
|
|
|
|
$lastNo = Quote::withTrashed()
|
|
->where('tenant_id', $tenantId)
|
|
->where('quote_number', 'like', "{$prefix}{$date}%")
|
|
->orderByDesc('quote_number')
|
|
->value('quote_number');
|
|
|
|
if ($lastNo) {
|
|
$seq = (int) substr($lastNo, -4) + 1;
|
|
} else {
|
|
$seq = 1;
|
|
}
|
|
|
|
return sprintf('%s%s%04d', $prefix, $date, $seq);
|
|
}
|
|
|
|
/**
|
|
* 견적번호 미리보기
|
|
*/
|
|
public function preview(?string $productCategory = null): array
|
|
{
|
|
$this->numberingService->setContext(
|
|
$this->tenantId(),
|
|
$this->apiUserId()
|
|
);
|
|
|
|
$rulePreview = $this->numberingService->preview('quote', [
|
|
'product_category' => $productCategory,
|
|
]);
|
|
|
|
if ($rulePreview !== null) {
|
|
return [
|
|
'quote_number' => $rulePreview['preview_number'],
|
|
'product_category' => $productCategory ?? Quote::CATEGORY_SCREEN,
|
|
'rule_name' => $rulePreview['rule_name'],
|
|
'generated_at' => now()->toDateTimeString(),
|
|
];
|
|
}
|
|
|
|
$quoteNumber = $this->generateLegacy($productCategory);
|
|
|
|
return [
|
|
'quote_number' => $quoteNumber,
|
|
'product_category' => $productCategory ?? Quote::CATEGORY_SCREEN,
|
|
'generated_at' => now()->toDateTimeString(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 견적번호 형식 검증
|
|
*
|
|
* 지원 형식:
|
|
* - 기본: QT{YYYYMMDD}{NNNN} (예: QT202602070001)
|
|
* - 채번규칙: KD-PR-{YYMMDD}-{NN} (예: KD-PR-260207-01)
|
|
*/
|
|
public function validate(string $quoteNumber): bool
|
|
{
|
|
return (bool) preg_match('/^(QT\d{8}\d{4,}|KD-[A-Z]{2}-\d{6}-\d{2,})$/', $quoteNumber);
|
|
}
|
|
|
|
/**
|
|
* 견적번호 파싱
|
|
*/
|
|
public function parse(string $quoteNumber): ?array
|
|
{
|
|
if (! $this->validate($quoteNumber)) {
|
|
return null;
|
|
}
|
|
|
|
// 채번규칙 형식: KD-PR-260207-01
|
|
if (str_starts_with($quoteNumber, 'KD-')) {
|
|
$parts = explode('-', $quoteNumber);
|
|
|
|
return [
|
|
'prefix' => $parts[0],
|
|
'category_code' => $parts[1],
|
|
'date' => $parts[2],
|
|
'sequence' => (int) $parts[3],
|
|
];
|
|
}
|
|
|
|
// 기본 형식: QT202602070001
|
|
return [
|
|
'prefix' => 'QT',
|
|
'date' => substr($quoteNumber, 2, 8),
|
|
'sequence' => (int) substr($quoteNumber, 10),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 견적번호 중복 체크
|
|
*/
|
|
public function isUnique(string $quoteNumber, ?int $excludeId = null): bool
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
$query = Quote::withTrashed()
|
|
->where('tenant_id', $tenantId)
|
|
->where('quote_number', $quoteNumber);
|
|
|
|
if ($excludeId) {
|
|
$query->where('id', '!=', $excludeId);
|
|
}
|
|
|
|
return ! $query->exists();
|
|
}
|
|
}
|