191 lines
6.2 KiB
PHP
191 lines
6.2 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\NumberingRule;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
class NumberingService extends Service
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* 채번 규칙 기반 번호 생성
|
||
|
|
*
|
||
|
|
* @param string $documentType 문서유형 (quote, order, sale, work_order, material_receipt)
|
||
|
|
* @param array $params 외부 파라미터 (pair_code, product_category 등)
|
||
|
|
* @return string|null 생성된 번호 (규칙 없으면 null → 호출자가 기존 로직 사용)
|
||
|
|
*/
|
||
|
|
public function generate(string $documentType, array $params = []): ?string
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
$rule = NumberingRule::withoutGlobalScopes()
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('document_type', $documentType)
|
||
|
|
->where('is_active', true)
|
||
|
|
->first();
|
||
|
|
|
||
|
|
if (! $rule) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$segments = $rule->pattern;
|
||
|
|
$result = '';
|
||
|
|
$scopeKey = '';
|
||
|
|
|
||
|
|
foreach ($segments as $segment) {
|
||
|
|
switch ($segment['type']) {
|
||
|
|
case 'static':
|
||
|
|
$result .= $segment['value'];
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'separator':
|
||
|
|
$result .= $segment['value'];
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'date':
|
||
|
|
$result .= now()->format($segment['format']);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'param':
|
||
|
|
$value = $params[$segment['key']] ?? $segment['default'] ?? '';
|
||
|
|
$result .= $value;
|
||
|
|
$scopeKey = $value;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'mapping':
|
||
|
|
$inputValue = $params[$segment['key']] ?? '';
|
||
|
|
$value = $segment['map'][$inputValue] ?? $segment['default'] ?? '';
|
||
|
|
$result .= $value;
|
||
|
|
$scopeKey = $value;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'sequence':
|
||
|
|
$periodKey = match ($rule->reset_period) {
|
||
|
|
'daily' => now()->format('ymd'),
|
||
|
|
'monthly' => now()->format('Ym'),
|
||
|
|
'yearly' => now()->format('Y'),
|
||
|
|
'never' => 'all',
|
||
|
|
default => now()->format('ymd'),
|
||
|
|
};
|
||
|
|
|
||
|
|
$nextSeq = $this->nextSequence(
|
||
|
|
$tenantId,
|
||
|
|
$documentType,
|
||
|
|
$scopeKey,
|
||
|
|
$periodKey
|
||
|
|
);
|
||
|
|
|
||
|
|
$result .= str_pad(
|
||
|
|
(string) $nextSeq,
|
||
|
|
$rule->sequence_padding,
|
||
|
|
'0',
|
||
|
|
STR_PAD_LEFT
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 미리보기 (시퀀스 증가 없이 다음 번호 예측)
|
||
|
|
*/
|
||
|
|
public function preview(string $documentType, array $params = []): ?array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
$rule = NumberingRule::withoutGlobalScopes()
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('document_type', $documentType)
|
||
|
|
->where('is_active', true)
|
||
|
|
->first();
|
||
|
|
|
||
|
|
if (! $rule) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$segments = $rule->pattern;
|
||
|
|
$result = '';
|
||
|
|
$scopeKey = '';
|
||
|
|
|
||
|
|
foreach ($segments as $segment) {
|
||
|
|
switch ($segment['type']) {
|
||
|
|
case 'static':
|
||
|
|
case 'separator':
|
||
|
|
$result .= $segment['value'];
|
||
|
|
break;
|
||
|
|
case 'date':
|
||
|
|
$result .= now()->format($segment['format']);
|
||
|
|
break;
|
||
|
|
case 'param':
|
||
|
|
$value = $params[$segment['key']] ?? $segment['default'] ?? '';
|
||
|
|
$result .= $value;
|
||
|
|
$scopeKey = $value;
|
||
|
|
break;
|
||
|
|
case 'mapping':
|
||
|
|
$inputValue = $params[$segment['key']] ?? '';
|
||
|
|
$value = $segment['map'][$inputValue] ?? $segment['default'] ?? '';
|
||
|
|
$result .= $value;
|
||
|
|
$scopeKey = $value;
|
||
|
|
break;
|
||
|
|
case 'sequence':
|
||
|
|
$periodKey = match ($rule->reset_period) {
|
||
|
|
'daily' => now()->format('ymd'),
|
||
|
|
'monthly' => now()->format('Ym'),
|
||
|
|
'yearly' => now()->format('Y'),
|
||
|
|
'never' => 'all',
|
||
|
|
default => now()->format('ymd'),
|
||
|
|
};
|
||
|
|
|
||
|
|
$currentSeq = (int) DB::table('numbering_sequences')
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('document_type', $documentType)
|
||
|
|
->where('scope_key', $scopeKey)
|
||
|
|
->where('period_key', $periodKey)
|
||
|
|
->value('last_sequence');
|
||
|
|
|
||
|
|
$result .= str_pad(
|
||
|
|
(string) ($currentSeq + 1),
|
||
|
|
$rule->sequence_padding,
|
||
|
|
'0',
|
||
|
|
STR_PAD_LEFT
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'preview_number' => $result,
|
||
|
|
'document_type' => $documentType,
|
||
|
|
'rule_name' => $rule->rule_name,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Atomic sequence increment (MySQL UPSERT)
|
||
|
|
*/
|
||
|
|
private function nextSequence(
|
||
|
|
int $tenantId,
|
||
|
|
string $documentType,
|
||
|
|
string $scopeKey,
|
||
|
|
string $periodKey
|
||
|
|
): int {
|
||
|
|
DB::statement(
|
||
|
|
'INSERT INTO numbering_sequences
|
||
|
|
(tenant_id, document_type, scope_key, period_key, last_sequence, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, 1, NOW(), NOW())
|
||
|
|
ON DUPLICATE KEY UPDATE
|
||
|
|
last_sequence = last_sequence + 1,
|
||
|
|
updated_at = NOW()',
|
||
|
|
[$tenantId, $documentType, $scopeKey, $periodKey]
|
||
|
|
);
|
||
|
|
|
||
|
|
return (int) DB::table('numbering_sequences')
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('document_type', $documentType)
|
||
|
|
->where('scope_key', $scopeKey)
|
||
|
|
->where('period_key', $periodKey)
|
||
|
|
->value('last_sequence');
|
||
|
|
}
|
||
|
|
}
|