# Player Ranking Sync Fix

## Overview

Perbaikan command `SyncPlayerRanking` agar hanya menyimpan role ke tabel `player_rankings` yang memiliki `userid` dan terdaftar di tabel `users` lokal. Mode `online` dihapus karena tidak diperlukan. Command `SyncFactionRanking` juga di-refactor mengikuti pola yang sama.

## Files

- `app/Console/Commands/SyncPlayerRanking.php`
- `app/Console/Commands/SyncFactionRanking.php`

## Masalah

1. **Mode online menyimpan semua role** — termasuk role yang tidak terdaftar di CP (`users` lokal)
2. **Mode all melakukan scan sequential ID** — mengiterasi ID 1, 2, 3... ke game API, sehingga menemukan user yang tidak ada di tabel `users` lokal (misal user ID 1040, 1072 padahal hanya 1024 yang terdaftar)
3. **Tidak ada validasi userid** — role tanpa `userid` atau role yang `userid`-nya tidak cocok tetap disimpan
4. **Kode tidak bersih** — duplikasi logic, method terlalu panjang, mode yang tidak perlu

## Solusi

### SyncPlayerRanking

| Perubahan | Detail |
|-----------|--------|
| Hapus mode `online` | Option `--mode` dihapus, command langsung sync dari tabel `users` lokal |
| Source data | Mengambil user IDs dari `DB::table('users')` bukan scan sequential |
| Validasi userid | `fetchRoleData()` menolak role yang `userid` kosong atau tidak cocok dengan expected user |
| Refactor method | Logic dipisah: `syncUserRoles()`, `fetchRoleData()`, `flushBatch()` |

**Signature baru:**
```
php artisan rankings:sync
    {--limit=0 : Maximum number of users to scan (0 = no limit)}
    {--batch=50 : Process batch size}
    {--delay=50 : Delay in milliseconds between API calls}
    {--start-id=0 : Starting user ID}
```

**Flow:**
1. Ambil daftar ID dari tabel `users` (filter `>= start-id`, limit jika diberikan)
2. Untuk setiap user ID, panggil `getRoles()` ke game API
3. Untuk setiap role, panggil `getRoleBase()` dan validasi `userid` cocok
4. Jika valid, ambil data lengkap (`getRoleStatus`, `getUserFaction`, `getFactionInfo`)
5. Simpan ke `player_rankings` via bulk upsert
6. Update `faction_rankings.total_pk_count` per batch

### SyncFactionRanking

| Perubahan | Detail |
|-----------|--------|
| Refactor `handle()` | `while` loop diganti `for` loop, `notFoundCount` check dipindah ke awal iterasi |
| Extract method | Logic fetch faction diextract ke `fetchFactionData()` |
| Bulk upsert | Disederhanakan pakai `array_fill` dan arrow function |
| Property class | `maxNotFound` dipindah ke property class |

## Validasi yang Diterapkan

```
fetchRoleData(roleId, userId):
  ├── getRoleBase(roleId) → valid?
  ├── base['name'] tidak kosong?
  ├── base['userid'] tidak kosong?
  ├── base['userid'] === userId?  ← cocok dengan user dari tabel users lokal
  └── Jika semua lolos → return data array
```

## Sebelum vs Sesudah

```
Sebelum:                                Sesudah:
--mode=online (tidak perlu)             Hanya satu mode: sync dari users lokal
--mode=all (scan sequential 1,2,3...)   Ambil ID dari DB::table('users')
Role tanpa userid tetap disimpan        Role tanpa userid ditolak
User tidak terdaftar di CP tersimpan    Hanya user terdaftar di CP yang disimpan
userid tidak divalidasi                 userid harus cocok dengan expected user
```
