# SOFTWARE DESIGN DOCUMENT: INTERNAL GENERATOR SURAT

Sistem Informasi Administrasi Desa (SIAD) - Desa Siofa Banua

---

## 1. PENDAHULUAN

Generator Surat merupakan subsistem internal berbasis layanan (Service-Oriented) di SIAD Desa Siofa Banua yang bertanggung jawab untuk memproses pengajuan surat, memetakan data penduduk dan desa ke template dokumen, membubuhkan nomor surat resmi, dan menghasilkan dokumen PDF siap cetak.

Sesuai dengan prinsip arsitektur bersih, Generator Surat **bukan merupakan menu atau UI controller**, melainkan sekumpulan Layanan Laravel (Service Classes) yang diisolasi dari logika presentasi (Controller) dan interaksi database langsung (diluar model orkestrasinya sendiri).

---

## 2. ARSITEKTUR & PRINSIP DESAIN

Desain internal Generator Surat didasarkan pada prinsip-prinsip berikut:
* **Single Responsibility Principle (SRP)**: Setiap layanan hanya bertanggung jawab atas satu domain tugas spesifik (misalnya, `NumberingService` hanya bertanggung jawab atas penomoran surat).
* **Dependency Injection (DI)**: Seluruh dependensi layanan diinjeksikan melalui constructor (Constructor Injection) untuk meningkatkan kemampuan pengujian (*testability*) dan fleksibilitas.
* **Singleton / Source of Truth**: Data Profil Desa bertindak sebagai satu-satunya sumber data resmi kewilayahan dan penandatangan surat yang valid (Singleton record pada tabel `village_profiles`).
* **Robust Exception Handling**: Kesalahan di tingkat internal (seperti data tidak lengkap atau kegagalan pihak ketiga) akan dilempar sebagai Exception spesifik untuk ditangani secara elegan di tingkat Controller.

---

## 3. DETAIL TANGGUNG JAWAB LAYANAN (SERVICES)

Generator Surat dibangun dari 4 (empat) layanan utama yang saling berkolaborasi:

```
┌─────────────────────────────────────────────────────────────────────────┐
│                        GeneratorSuratService                            │
│                             (Orchestrator)                              │
└─────────────────────────────────────────────────────────────────────────┘
        │                        │                        │
        ▼                        ▼                        ▼
┌──────────────┐         ┌──────────────┐         ┌──────────────┐
│  Numbering   │         │ Placeholder  │         │     Pdf      │
│   Service    │         │   Service    │         │  Generator   │
│              │         │              │         │   Service    │
└──────────────┘         └──────────────┘         └──────────────┘
```

### 1. `GeneratorSuratService` (Orchestrator)
* **Tanggung Jawab**:
  * Menjadi pintu masuk utama proses pembentukan surat resmi dari Controller.
  * Mengambil data `PengajuanSurat`, `Penduduk`, `VillageProfile`, dan `MasterSurat` aktif.
  * Mengatur rantai eksekusi sub-layanan (memanggil `NumberingService`, lalu `PlaceholderService`, kemudian `PdfGeneratorService`).
  * Menyimpan berkas PDF fisik hasil generator ke public storage disk.
  * Mengembalikan Data Transfer Object (DTO) `GeneratorResult` kepada pemanggil (Controller).

### 2. `NumberingService`
* **Tanggung Jawab**:
  * Menghitung nomor urut resmi berikutnya secara aman menggunakan transaksi database (*database transaction*) dan penguncian baris data (*pessimistic lock*) untuk mencegah kondisi perlombaan (*race conditions* / nomor ganda).
  * Menyusun format nomor surat resmi desa: `470/{nomor_urut}/{kode_surat}/{bulan_romawi}/{tahun}`.
  * Memperbarui counter nomor terakhir pada tabel `penomoran_surats`.

### 3. `PlaceholderService`
* **Tanggung Jawab**:
  * Memetakan data model (Penduduk, Pengajuan, Profil Desa, Nomor) ke format array placeholder standard `snake_case` huruf kecil.
  * Memvalidasi bahwa seluruh placeholder wajib yang digunakan di dalam template surat memiliki nilai yang sah (tidak null).
  * Memvalidasi bahwa tidak ada placeholder yang tidak dikenali/tidak terdaftar di dalam template.
  * Melakukan penggantian string/HTML menggunakan ekspresi reguler (Regex Callback) yang efisien.

### 4. `PdfGeneratorService`
* **Tanggung Jawab**:
  * Menerima string HTML final (yang siap dicetak) beserta konfigurasi halaman.
  * Memproses render HTML menjadi dokumen PDF biner menggunakan pustaka `Barryvdh\DomPDF\Facade\Pdf`.
  * Mengelola penyisipan stylesheet khusus cetak A4.

---

## 4. DEFINISI CLASS, INTERFACE, & METODE

### A. DTO: `GeneratorResult`

Objek pembungkus keluaran agar tipe data terjamin aman (*type-safe*).

```php
namespace App\Services\Dto;

class GeneratorResult
{
    public function __construct(
        public readonly string $nomorSurat,
        public readonly string $filePath,
        public readonly string $filename,
        public readonly bool $isSuccess
    ) {}
}
```

### B. Orchestrator: `GeneratorSuratService`

```php
namespace App\Services;

use App\Models\PengajuanSurat;
use App\Services\Dto\GeneratorResult;
use App\Exceptions\TemplateNotFoundException;
use App\Exceptions\VillageProfileNotSetException;
use App\Exceptions\ResidentNotFoundException;
use App\Exceptions\PdfGenerationFailedException;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class GeneratorSuratService
{
    public function __construct(
        protected NumberingService $numberingService,
        protected PlaceholderService $placeholderService,
        protected PdfGeneratorService $pdfGeneratorService
    ) {}

    /**
     * Mengorkestrasikan pembuatan dokumen surat resmi.
     * 
     * @param PengajuanSurat $pengajuan
     * @return GeneratorResult
     * @throws \Exception
     */
    public function generate(PengajuanSurat $pengajuan): GeneratorResult
    {
        // 1. Ambil data Penduduk & validasi
        $penduduk = $pengajuan->penduduk;
        if (!$penduduk) {
            throw new ResidentNotFoundException("Data penduduk pengaju tidak ditemukan.");
        }

        // 2. Ambil data template aktif dari MasterSurat
        $template = $pengajuan->jenisSurat->masterSurat;
        if (!$template || $template->status !== 'aktif') {
            throw new TemplateNotFoundException("Template Master Surat aktif untuk jenis surat ini tidak ditemukan.");
        }

        // 3. Ambil data Profil Desa & validasi
        $profile = \App\Models\VillageProfile::first();
        if (!$profile) {
            throw new VillageProfileNotSetException("Profil Desa belum diisi. Silakan lengkapi terlebih dahulu.");
        }

        // 4. Generate Nomor Surat Resmi melalui NumberingService
        $nomorSurat = $this->numberingService->generate($template);

        // 5. Susun payload data gabungan untuk disubstitusikan ke template
        $dataPayload = $this->preparePayload($pengajuan, $penduduk, $profile, $nomorSurat);

        // 6. Ganti placeholder di template menggunakan PlaceholderService
        $htmlContent = $this->placeholderService->replace($template->isi_template, $dataPayload);

        // 7. Render menjadi file PDF melalui PdfGeneratorService
        $pdfBinary = $this->pdfGeneratorService->generatePdfFromHtml($htmlContent);
        if (!$pdfBinary) {
            throw new PdfGenerationFailedException("Gagal me-render dokumen PDF.");
        }

        // 8. Simpan file PDF ke public storage disk
        $filename = 'surat_' . Str::slug($template->nama_surat) . '_' . $pengajuan->nomor_pengajuan . '.pdf';
        $relativeFolder = 'surat-keluar/' . date('Y/m');
        $fullPath = $relativeFolder . '/' . $filename;

        Storage::disk('public')->put($fullPath, $pdfBinary);

        return new GeneratorResult(
            nomorSurat: $nomorSurat,
            filePath: $fullPath,
            filename: $filename,
            isSuccess: true
        );
    }

    /**
     * Mempersiapkan payload data terpadu untuk placeholder.
     */
    protected function preparePayload(
        PengajuanSurat $pengajuan, 
        \App\Models\Penduduk $penduduk, 
        \App\Models\VillageProfile $profile, 
        string $nomorSurat
    ): array {
        return [
            'nomor_surat' => $nomorSurat,
            'tanggal_surat' => \Carbon\Carbon::now()->translatedFormat('d F Y'),
            
            // Data Penduduk
            'nama_lengkap' => $penduduk->nama_lengkap,
            'nik' => $penduduk->nik,
            'nomor_kk' => $penduduk->kartuKeluarga->no_kk ?? '-',
            'tempat_lahir' => $penduduk->tempat_lahir,
            'tanggal_lahir' => \Carbon\Carbon::parse($penduduk->tanggal_lahir)->translatedFormat('d F Y'),
            'jenis_kelamin' => $penduduk->jenis_kelamin,
            'agama' => $penduduk->agama,
            'pekerjaan' => $penduduk->pekerjaan ?: '-',
            'status_perkawinan' => $penduduk->status_perkawinan ?: '-',
            'kewarganegaraan' => $penduduk->kewarganegaraan ?: 'WNI',
            'alamat' => TemplateParserService::constructAddress($penduduk),

            // Data Pengajuan
            'keperluan' => $pengajuan->keperluan ?: '-',
            
            // Profil Desa
            'nama_desa' => $profile->village_name,
            'alamat_desa' => $profile->address,
            'kecamatan' => $profile->district_name,
            'kabupaten' => $profile->regency_name,
            'provinsi' => $profile->province_name,
            'nama_penandatangan' => $profile->signer_name,
            'jabatan_penandatangan' => $profile->signer_position,
        ];
    }
}
```

### C. Penomoran: `NumberingService`

```php
namespace App\Services;

use App\Models\MasterSurat;
use App\Models\PenomoranSurat;
use App\Exceptions\NumberGenerationFailedException;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;

class NumberingService
{
    /**
     * Menghasilkan nomor surat berikutnya secara urut dan aman dari race condition.
     */
    public function generate(MasterSurat $template): string
    {
        $now = Carbon::now();
        $year = $now->year;
        $monthRomawi = $this->getRomawi($now->month);
        $code = $template->kode_surat;

        if (!$code) {
            throw new NumberGenerationFailedException("Kode Surat pada Template Master tidak boleh kosong.");
        }

        // Jalankan database transaction dengan pessimistic lock pada counter nomor_terakhir
        $number = DB::transaction(function () use ($code, $year) {
            $penomoran = PenomoranSurat::where('kode_surat', $code)
                ->where('tahun', $year)
                ->lockForUpdate() // Mengunci baris ini agar proses lain mengantri
                ->first();

            if (!$penomoran) {
                $penomoran = PenomoranSurat::create([
                    'kode_surat' => $code,
                    'tahun' => $year,
                    'nomor_terakhir' => 0
                ]);
            }

            $nextNum = $penomoran->nomor_terakhir + 1;

            // Pastikan tidak ada surat yang secara tidak sengaja menggunakan nomor ini (safety check)
            while (true) {
                $formattedNumber = str_pad($nextNum, 3, '0', STR_PAD_LEFT);
                $candidateNomor = "470/{$formattedNumber}/{$code}/{$this->getRomawi(Carbon::now()->month)}/{$year}";
                $exists = \App\Models\SuratCetak::where('nomor_surat', $candidateNomor)->exists();
                if (!$exists) {
                    break;
                }
                $nextNum++;
            }

            $penomoran->nomor_terakhir = $nextNum;
            $penomoran->save();

            return $nextNum;
        });

        $formattedNumber = str_pad($number, 3, '0', STR_PAD_LEFT);
        
        return "470/{$formattedNumber}/{$code}/{$monthRomawi}/{$year}";
    }

    private function getRomawi(int $month): string
    {
        $romawi = [
            1 => 'I', 2 => 'II', 3 => 'III', 4 => 'IV', 5 => 'V', 6 => 'VI',
            7 => 'VII', 8 => 'VIII', 9 => 'IX', 10 => 'X', 11 => 'XI', 12 => 'XII'
        ];
        return $romawi[$month] ?? 'I';
    }
}
```

### D. Parsing: `PlaceholderService`

```php
namespace App\Services;

use App\Exceptions\MissingRequiredDataException;
use App\Exceptions\UnrecognizedPlaceholderException;

class PlaceholderService
{
    // Daftar placeholder resmi yang wajib didukung sistem
    protected array $supportedPlaceholders = [
        'nomor_surat', 'tanggal_surat', 'nama_lengkap', 'nik', 'nomor_kk', 
        'tempat_lahir', 'tanggal_lahir', 'jenis_kelamin', 'alamat', 
        'pekerjaan', 'agama', 'status_perkawinan', 'kewarganegaraan', 
        'keperluan', 'nama_desa', 'alamat_desa', 'kecamatan', 'kabupaten', 
        'provinsi', 'nama_penandatangan', 'jabatan_penandatangan'
    ];

    // Placeholder yang wajib ada isinya (tidak boleh null/empty) apabila dipanggil di template
    protected array $requiredFields = [
        'nama_lengkap', 'nik', 'alamat', 'nama_desa', 
        'nama_penandatangan', 'jabatan_penandatangan'
    ];

    /**
     * Mengganti placeholder di template dengan data real.
     */
    public function replace(string $template, array $data): string
    {
        // 1. Ekstrak seluruh placeholder yang ada di dalam template
        preg_match_all('/\{\{\s*(\w+)\s*\}\}/', $template, $matches);
        $placeholdersInTemplate = array_unique($matches[1] ?? []);

        // 2. Validasi Keberadaan & Kesahihan Placeholder
        foreach ($placeholdersInTemplate as $placeholder) {
            $normalizedPlaceholder = strtolower($placeholder);

            // Jika placeholder tidak dikenali sistem, lempar exception
            if (!in_array($normalizedPlaceholder, $this->supportedPlaceholders)) {
                throw new UnrecognizedPlaceholderException("Placeholder '{{ {$placeholder} }}' tidak dikenali oleh sistem.");
            }

            // Jika placeholder adalah data wajib tapi tidak disediakan di data payload, lempar exception
            if (in_array($normalizedPlaceholder, $this->requiredFields)) {
                if (!isset($data[$normalizedPlaceholder]) || trim($data[$normalizedPlaceholder]) === '') {
                    throw new MissingRequiredDataException("Data wajib '{$normalizedPlaceholder}' untuk placeholder tidak tersedia.");
                }
            }
        }

        // 3. Lakukan penggantian string secara efisien
        return preg_replace_callback('/\{\{\s*(\w+)\s*\}\}/', function ($matches) use ($data) {
            $key = strtolower($matches[1]);
            return $data[$key] ?? '';
        }, $template);
    }
}
```

### E. Render PDF: `PdfGeneratorService`

```php
namespace App\Services;

use Barryvdh\DomPDF\Facade\Pdf;

class PdfGeneratorService
{
    /**
     * Merender string HTML template menjadi file PDF biner.
     */
    public function generatePdfFromHtml(string $htmlContent): string
    {
        // Siapkan dokumen HTML terstruktur dengan font pendukung dan margins A4
        $styledHtml = $this->applyPrintStyles($htmlContent);

        $pdf = Pdf::loadHTML($styledHtml);
        $pdf->setPaper('a4', 'portrait');

        return $pdf->output();
    }

    /**
     * Menyematkan styling CSS A4 print standard.
     */
    protected function applyPrintStyles(string $htmlContent): string
    {
        return "
        <!DOCTYPE html>
        <html>
        <head>
            <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
            <style>
                @page {
                    margin: 2cm 2cm 2cm 2cm;
                }
                body {
                    font-family: 'Times New Roman', Times, serif;
                    font-size: 12pt;
                    line-height: 1.5;
                    color: #000;
                }
                p {
                    margin-bottom: 1em;
                    text-align: justify;
                }
                .text-center { text-align: center; }
                .text-right { text-align: right; }
                .font-weight-bold { font-weight: bold; }
                .noborder-table td {
                    border: none;
                    padding: 3px 0;
                }
            </style>
        </head>
        <body>
            {$htmlContent}
        </body>
        </html>";
    }
}
```

---

## 5. ALUR KOMUNIKASI & SEQUENCE DIAGRAM

Diagram urutan pemrosesan dari Controller admin (saat menyetujui surat) hingga surat biner diterbitkan.

```
[Controller]           [GeneratorSuratService]        [NumberingService]        [PlaceholderService]        [PdfGeneratorService]
     │                            │                           │                          │                           │
     │─── generate($pengajuan) ──>│                           │                          │                           │
     │                            │─── generate($template) ──>│                          │                           │
     │                            │<─── [Nomor Surat] ────────│                          │                           │
     │                            │                                                      │                           │
     │                            │───────────── replace($templateHTML, $data) ─────────>│                           │
     │                            │<──────────── [HTML Final] ───────────────────────────│                           │
     │                            │                                                                                  │
     │                            │────────────────────────── generatePdfFromHtml($html) ───────────────────────────>│
     │                            │<───────────────────────── [PDF Binary] ──────────────────────────────────────────│
     │                            │
     │─── [GeneratorResult] ─────>│
     ▼                            ▼
```

---

## 6. MANAJEMEN EXCEPTION (ERROR HANDLING)

Berikut adalah daftar Exception khusus yang dipicu jika alur generator mendeteksi anomali:

| Exception | Deskripsi | Status HTTP / Tindakan |
|---|---|---|
| `ResidentNotFoundException` | Penduduk tidak ditemukan untuk data pengaju surat. | 404 Not Found (Redirect back dengan toast error) |
| `TemplateNotFoundException` | Template Master Surat aktif tidak disetujui / tidak ada. | 400 Bad Request (Peringatan di dashboard admin) |
| `VillageProfileNotSetException` | Profil Desa belum disemai atau kosong di database. | 500 Server Error (Mengarahkan admin ke Pengaturan) |
| `NumberGenerationFailedException` | Konfigurasi nomor kode surat kosong atau database bermasalah. | 500 Server Error (Rollback transaksi DB) |
| `MissingRequiredDataException` | Data krusial penduduk (seperti NIK) null di database saat dipanggil. | 422 Unprocessable Entity (Detail error form) |
| `UnrecognizedPlaceholderException` | Template mengandung markup asing diluar 21 placeholder resmi. | 422 Unprocessable Entity (Gagal menyimpan Master Surat) |
| `PdfGenerationFailedException` | Dompdf mengalami kegagalan parsing dokumen biner. | 500 Server Error (Log error dicatat) |

---

## 7. STRUKTUR DEPENDENSI (CLASS DIAGRAM)

```
                       ┌─────────────────────────┐
                       │  GeneratorSuratService  │
                       └─────────────────────────┘
                                    │
           ┌────────────────────────┼────────────────────────┐
           ▼                        ▼                        ▼
┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│ NumberingService │     │PlaceholderService│     │PdfGeneratorServ. │
└──────────────────┘     └──────────────────┘     └──────────────────┘
           │                                                 │
           ▼                                                 ▼
┌──────────────────┐                               ┌──────────────────┐
│  PenomoranSurat  │                               │  Dompdf / Barry  │
└──────────────────┘                               └──────────────────┘
```