subDays($days); $rows = Purchase::where('supplier_id', $supplier->id) ->where('status', 'received') ->whereNotNull('expected_at') ->whereNotNull('received_at') ->where('received_at', '>=', $since) ->get(['expected_at', 'received_at']); if ($rows->isEmpty()) return null; $onTime = $rows->filter(fn ($r) => $r->received_at->lte($r->expected_at))->count(); return round($onTime / $rows->count() * 100, 1); } /** Average days between order_date and received_at. */ public function avgDeliveryDays(Supplier $supplier, int $days = 90): ?float { $since = Carbon::now()->subDays($days); $rows = Purchase::where('supplier_id', $supplier->id) ->where('status', 'received') ->whereNotNull('order_date') ->whereNotNull('received_at') ->where('received_at', '>=', $since) ->get(['order_date', 'received_at']); if ($rows->isEmpty()) return null; $total = $rows->sum(fn ($r) => $r->order_date->diffInDays($r->received_at)); return round($total / $rows->count(), 1); } /** Total spend (sum of purchase totals) over a window. */ public function spend(Supplier $supplier, int $days = 90): float { $since = Carbon::now()->subDays($days); return (float) Purchase::where('supplier_id', $supplier->id) ->where('status', 'received') ->where('received_at', '>=', $since) ->sum('total'); } /** Count of received purchases in window. */ public function count(Supplier $supplier, int $days = 90): int { $since = Carbon::now()->subDays($days); return (int) Purchase::where('supplier_id', $supplier->id) ->where('status', 'received') ->where('received_at', '>=', $since) ->count(); } /** * Compose a 1-5 score from on-time, delivery speed and spend volume. * Returns null when not enough signal — caller may keep manual rating. */ public function computedRating(Supplier $supplier, int $days = 90): ?int { $onTime = $this->onTimeRate($supplier, $days); $count = $this->count($supplier, $days); if ($onTime === null || $count < 2) return null; // need at least 2 deliveries to rate // On-time is the dominant factor (0..70 points). $score = $onTime * 0.7; // Bonus for higher volume (0..20 points capped at 10 purchases). $score += min(20, $count * 2); // Speed bonus: faster than 7 days avg → +10. $avg = $this->avgDeliveryDays($supplier, $days); if ($avg !== null && $avg < 7) $score += 10; return (int) max(1, min(5, round($score / 20))); } }