feat: sam_stat P1 도메인 확장 (Phase 3)
- 차원 테이블: dim_client, dim_product 마이그레이션 + SCD Type 2 동기화 (DimensionSyncService) - 재고 통계: stat_inventory_daily + InventoryStatService (stocks, stock_transactions, inspections) - 견적/영업 통계: stat_quote_pipeline_daily + QuoteStatService (quotes, biddings, sales_prospects) - 인사/근태 통계: stat_hr_attendance_daily + HrStatService (attendances, leaves, user_tenants) - KPI/알림: stat_kpi_targets, stat_alerts + KpiAlertService + StatCheckKpiAlertsCommand - StatAggregatorService에 inventory, quote, hr 도메인 추가 (총 6개 도메인) - 스케줄러: stat:check-kpi-alerts 매일 09:00 등록 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
97
app/Services/Stats/InventoryStatService.php
Normal file
97
app/Services/Stats/InventoryStatService.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Stats;
|
||||
|
||||
use App\Models\Stats\Daily\StatInventoryDaily;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InventoryStatService implements StatDomainServiceInterface
|
||||
{
|
||||
public function aggregateDaily(int $tenantId, Carbon $date): int
|
||||
{
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
|
||||
// 재고 현황 (stocks 테이블 - 현재 스냅샷)
|
||||
$stockSummary = DB::connection('mysql')
|
||||
->table('stocks')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('
|
||||
COUNT(*) as sku_count,
|
||||
COALESCE(SUM(stock_qty), 0) as total_qty,
|
||||
SUM(CASE WHEN stock_qty < safety_stock AND safety_stock > 0 THEN 1 ELSE 0 END) as below_safety,
|
||||
SUM(CASE WHEN stock_qty = 0 THEN 1 ELSE 0 END) as zero_stock,
|
||||
SUM(CASE WHEN stock_qty > safety_stock * 3 AND safety_stock > 0 THEN 1 ELSE 0 END) as excess_stock
|
||||
')
|
||||
->first();
|
||||
|
||||
// 입고 (stock_transactions type = 'receipt')
|
||||
$receiptStats = DB::connection('mysql')
|
||||
->table('stock_transactions')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->where('type', 'receipt')
|
||||
->selectRaw('COUNT(*) as cnt, COALESCE(SUM(qty), 0) as total_qty')
|
||||
->first();
|
||||
|
||||
// 출고 (stock_transactions type = 'issue')
|
||||
$issueStats = DB::connection('mysql')
|
||||
->table('stock_transactions')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->where('type', 'issue')
|
||||
->selectRaw('COUNT(*) as cnt, COALESCE(SUM(ABS(qty)), 0) as total_qty')
|
||||
->first();
|
||||
|
||||
// 품질검사 (inspections)
|
||||
$inspectionStats = DB::connection('mysql')
|
||||
->table('inspections')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('inspection_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw("
|
||||
COUNT(*) as cnt,
|
||||
SUM(CASE WHEN result = 'pass' THEN 1 ELSE 0 END) as pass_count,
|
||||
SUM(CASE WHEN result = 'fail' THEN 1 ELSE 0 END) as fail_count
|
||||
")
|
||||
->first();
|
||||
|
||||
$inspectionCount = $inspectionStats->cnt ?? 0;
|
||||
$passCount = $inspectionStats->pass_count ?? 0;
|
||||
$failCount = $inspectionStats->fail_count ?? 0;
|
||||
$passRate = $inspectionCount > 0 ? ($passCount / $inspectionCount) * 100 : 0;
|
||||
|
||||
StatInventoryDaily::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_date' => $dateStr],
|
||||
[
|
||||
'total_sku_count' => $stockSummary->sku_count ?? 0,
|
||||
'total_stock_qty' => $stockSummary->total_qty ?? 0,
|
||||
'total_stock_value' => 0, // 단가 정보 없어 Phase 4에서 보완
|
||||
'receipt_count' => $receiptStats->cnt ?? 0,
|
||||
'receipt_qty' => $receiptStats->total_qty ?? 0,
|
||||
'receipt_amount' => 0,
|
||||
'issue_count' => $issueStats->cnt ?? 0,
|
||||
'issue_qty' => $issueStats->total_qty ?? 0,
|
||||
'issue_amount' => 0,
|
||||
'below_safety_count' => $stockSummary->below_safety ?? 0,
|
||||
'zero_stock_count' => $stockSummary->zero_stock ?? 0,
|
||||
'excess_stock_count' => $stockSummary->excess_stock ?? 0,
|
||||
'inspection_count' => $inspectionCount,
|
||||
'inspection_pass_count' => $passCount,
|
||||
'inspection_fail_count' => $failCount,
|
||||
'inspection_pass_rate' => $passRate,
|
||||
'turnover_rate' => 0, // 월간 집계에서 계산
|
||||
]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function aggregateMonthly(int $tenantId, int $year, int $month): int
|
||||
{
|
||||
// 재고 도메인은 일간 스냅샷 기반이므로 별도 월간 테이블 없음
|
||||
// 필요시 Phase 4에서 stat_inventory_monthly 추가
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user