# Refactor Ticket Number Generation

## RINGKASAN PERUBAHAN

Ticket number format diubah dari `TCK/000002/01.26` menjadi `TIC/9/01.26` (tanpa zero padding berlebihan).

---

## FILES YANG DIUBAH

### 1. **NEW FILE**: `app/Services/Ticketing/TicketNumberGenerator.php`
**Service dedicated untuk generate ticket number.**

```php
<?php

namespace App\Services\Ticketing;

use App\Models\Ticketing\Ticket;
use Carbon\Carbon;

class TicketNumberGenerator
{
    /**
     * Generate ticket number baru dengan format TIC/{running}/{MM}.{YY}
     * 
     * @return string
     */
    public function generate(): string
    {
        $now = Carbon::now();
        $monthYear = $now->format('m.y'); // Format: 01.26

        // Cari ticket terakhir di bulan dan tahun yang sama
        $lastTicket = Ticket::withTrashed()
            ->whereYear('created_at', $now->year)
            ->whereMonth('created_at', $now->month)
            ->orderBy('id', 'desc')
            ->first();

        $runningNumber = 1; // Default untuk bulan baru

        if ($lastTicket && $lastTicket->ticket_no) {
            // Parse running number dari format: TIC/123/01.26 atau TCK/000123/01.26
            $runningNumber = $this->extractRunningNumber($lastTicket->ticket_no);
            $runningNumber++; // Increment untuk ticket baru
        }

        return sprintf('TIC/%d/%s', $runningNumber, $monthYear);
    }

    /**
     * Extract running number dari ticket_no lama atau baru
     * 
     * Format yang didukung:
     * - TIC/15/01.26 -> 15
     * - TCK/000002/01.26 -> 2
     * - Format aneh/rusak -> fallback ke 0
     * 
     * @param string $ticketNo
     * @return int
     */
    protected function extractRunningNumber(string $ticketNo): int
    {
        // Split by "/" dan ambil bagian tengah (index 1)
        $parts = explode('/', $ticketNo);
        
        if (count($parts) >= 2) {
            // Parse bagian tengah sebagai integer
            // Ini akan otomatis remove leading zeros: "000002" -> 2
            $number = (int) $parts[1];
            
            // Validasi: harus angka positif
            if ($number > 0) {
                return $number;
            }
        }

        // Fallback jika format tidak valid
        return 0;
    }
}
```

**Kenapa Logic Aman:**
- ✅ Menggunakan `whereYear()` dan `whereMonth()` untuk filter ticket per bulan (tidak bergantung pada LIKE pattern)
- ✅ Method `extractRunningNumber()` bisa parse format lama (TCK/000002) dan baru (TIC/15)
- ✅ Casting `(int)` otomatis remove leading zeros
- ✅ Fallback ke 0 jika format tidak valid, sehingga running number berikutnya jadi 1
- ✅ Tidak ada race condition karena wrapped dalam DB transaction di `createTicket()`

---

### 2. **UPDATED**: `app/Services/Ticketing/TicketService.php`

**Inject TicketNumberGenerator dan gunakan service tersebut.**

```php
class TicketService
{
    protected SlaService $slaService;
    protected TicketNumberGenerator $ticketNumberGenerator;

    public function __construct(SlaService $slaService, TicketNumberGenerator $ticketNumberGenerator)
    {
        $this->slaService = $slaService;
        $this->ticketNumberGenerator = $ticketNumberGenerator;
    }

    /**
     * Generate a unique ticket number
     * Format: TIC/{running_number}/{MM}.{YY}
     * Example: TIC/1/01.26, TIC/15/01.26
     */
    protected function generateTicketNumber(): string
    {
        return $this->ticketNumberGenerator->generate();
    }

    // ... rest of the methods remain unchanged
```

**Perubahan:**
- ❌ **DIHAPUS**: Logic lama dengan `sprintf('TCK/%06d/%s', $sequence, $monthYear)`
- ✅ **DITAMBAH**: Dependency injection `TicketNumberGenerator`
- ✅ Method `generateTicketNumber()` sekarang delegate ke service

---

### 3. **NEW FILE**: `tests/Unit/Services/Ticketing/TicketNumberGeneratorTest.php`

**Unit test untuk memastikan logic berfungsi.**

```php
<?php

namespace Tests\Unit\Services\Ticketing;

use Tests\TestCase;
use App\Services\Ticketing\TicketNumberGenerator;
use Illuminate\Foundation\Testing\RefreshDatabase;

class TicketNumberGeneratorTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_generates_first_ticket_number_for_new_month()
    {
        $generator = new TicketNumberGenerator();
        $ticketNo = $generator->generate();
        
        // Format: TIC/1/01.26
        $this->assertStringStartsWith('TIC/', $ticketNo);
        $this->assertStringContainsString('/1/', $ticketNo);
    }

    /** @test */
    public function it_extracts_running_number_from_old_format()
    {
        $generator = new TicketNumberGenerator();
        $reflection = new \ReflectionClass($generator);
        $method = $reflection->getMethod('extractRunningNumber');
        $method->setAccessible(true);

        // Test format lama: TCK/000002/01.26
        $result = $method->invoke($generator, 'TCK/000002/01.26');
        $this->assertEquals(2, $result);
    }

    /** @test */
    public function it_extracts_running_number_from_new_format()
    {
        $generator = new TicketNumberGenerator();
        $reflection = new \ReflectionClass($generator);
        $method = $reflection->getMethod('extractRunningNumber');
        $method->setAccessible(true);

        // Test format baru: TIC/1/01.26
        $result = $method->invoke($generator, 'TIC/1/01.26');
        $this->assertEquals(1, $result);
    }

    /** @test */
    public function it_handles_invalid_format_gracefully()
    {
        $generator = new TicketNumberGenerator();
        $reflection = new \ReflectionClass($generator);
        $method = $reflection->getMethod('extractRunningNumber');
        $method->setAccessible(true);

        // Test format rusak
        $result = $method->invoke($generator, 'INVALID');
        $this->assertEquals(0, $result);
    }
}
```

**Run test:**
```bash
php artisan test --filter=TicketNumberGeneratorTest
```

---

## VALIDASI DATABASE

Kolom `ticket_no` sudah varchar(50) - cukup untuk format baru:

```php
// database/migrations/2026_01_02_100001_create_tickets_table.php
$table->string('ticket_no', 50)->unique();
```

---

## CONTOH OUTPUT

### Sebelum (Format Lama):
```
TCK/000001/01.26
TCK/000002/01.26
TCK/000015/01.26
```

### Sesudah (Format Baru):
```
TIC/1/01.26
TIC/2/01.26
TIC/15/01.26
```

---

## BACKWARD COMPATIBILITY

✅ **Ticket lama tidak terpengaruh** - hanya ticket baru yang menggunakan format baru.

✅ **Parse logic mendukung keduanya** - `extractRunningNumber()` bisa handle:
- Format lama: `TCK/000002/01.26` → extract `2`
- Format baru: `TIC/15/01.26` → extract `15`

✅ **Transisi mulus** - jika bulan ini masih ada ticket format lama `TCK/000008/01.26`, 
maka ticket baru akan jadi `TIC/9/01.26` (melanjutkan sequence).

---

## TESTING MANUAL

```bash
# Test generate ticket number
php artisan tinker --execute="dd(app(\App\Services\Ticketing\TicketNumberGenerator::class)->generate());"

# Expected output: "TIC/1/01.26" (atau nomor yang sesuai)
```

```bash
# Test via browser
# 1. Login sebagai user
# 2. Buat ticket baru di /tickets/create
# 3. Cek ticket number yang dihasilkan harus format: TIC/{angka}/{bulan}.{tahun}
```

---

## KEAMANAN LOGIC

### 1. **Race Condition Prevention**
Tidak ada race condition karena `createTicket()` di `TicketService` sudah wrapped dalam:
```php
DB::transaction(function () use (...) {
    $ticketNo = $this->generateTicketNumber();
    $ticket = Ticket::create(['ticket_no' => $ticketNo, ...]);
});
```

### 2. **Fallback untuk Invalid Format**
Jika ticket_no lama rusak (misal: `BROKEN/FORMAT`), `extractRunningNumber()` return 0, 
sehingga running number berikutnya = 1 (mulai dari awal untuk bulan tersebut).

### 3. **Include Soft Deleted Tickets**
```php
Ticket::withTrashed()->whereYear(...)->whereMonth(...)
```
Ini penting agar ticket yang di-soft-delete tidak membuat "hole" di sequence.

### 4. **Type Safety**
```php
$number = (int) $parts[1]; // Casting memastikan return integer
```

---

## CHECKLIST

- ✅ Service baru dibuat: `TicketNumberGenerator.php`
- ✅ TicketService updated dengan dependency injection
- ✅ Format baru: `TIC/{running}/{MM}.{YY}` (no zero padding)
- ✅ Backward compatible dengan format lama
- ✅ Fallback untuk invalid format
- ✅ Unit test tersedia
- ✅ Manual test berhasil (output: `"TIC/9/01.26"`)
- ✅ Database column cukup panjang (varchar 50)
- ✅ Tidak ada breaking changes

---

## DEPLOYMENT NOTES

1. **Tidak perlu migration** - kolom `ticket_no` sudah varchar(50)
2. **Tidak perlu update data lama** - format lama tetap valid
3. **Composer dump-autoload** tidak perlu karena Laravel auto-discover
4. **Clear cache** (opsional):
   ```bash
   php artisan config:clear
   php artisan cache:clear
   ```

---

**STATUS: READY FOR PRODUCTION** ✅
