# SAP Sync Report (`rptsap`)

## Overview

Report **`rptsap`** adalah halaman khusus untuk melakukan **pull/sync data dari SAP** ke database lokal. Fitur ini dipisahkan dari `rptptr` agar `rptptr` tetap sebagai report viewer ZVBILLING (Price Trend), sementara proses sinkronisasi SAP memiliki tempatnya sendiri.

`rptsap` mendukung sinkronisasi untuk 8 endpoint SAP secara terpisah:

| Report Type | Label | Tabel Tujuan |
|---|---|---|
| `ZVBILLING` | Billing (ZVBILLING) | `rpt_ET_ZVBILLING` |
| `ZFFINANCE_YIELD` | Finance Yield (ZFFINANCE_YIELD_ACCOUNTING) | `rpt_et_zvfinance_yield_accounting` |
| `Z_GET_NEW_CUST` | Customer Master (Z_GET_NEW_CUST) | `rpt_et_zvgetncust` |
| `Z_GET_PAYMENT` | Payment (Z_GET_PAYMENT) | `rpt_et_zvgetpaybill` |
| `Z_GET_ONHAND` | Sales On Hand (ZFM_RF_GET_ONHAND) | `rpt_et_getonhand` |
| `Z_GET_DOBYSO` | DO by SO (ZFM_RF_GET_DOBYSO) | `rpt_et_getdobyso` |
| `Z_GET_TCURR` | Exchange Rate (Z_GET_TCURR) | `rpt_et_gettcurr` |
| `Z_GET_SOBYRANGE` | SO by Range (ZFM_SALESORDERGETRANGE) | `rpt_et_getsobyrange` |

> **Filter tahun & loop bulan otomatis**:
> - Semua endpoint di-report menggunakan filter **Tahun** saja.
> - `Z_GET_ONHAND`, `Z_GET_DOBYSO` dan `Z_GET_NEW_CUST` secara internal melakukan loop per bulan dari **Januari sampai bulan berjalan** sesuai tahun yang dipilih.
> - `Z_GET_TCURR` hanya melakukan satu panggilan per tahun (parameter `IV_YEAR`), karena data exchange rate dikembalikan langsung untuk seluruh tahun.
> - Tidak ada field bulan di UI; backend menentukan sendiri bulan akhir (current month untuk tahun berjalan, Desember untuk tahun lampau).

---

## File Structure

```
app/
  Http/
    Controllers/
      RptsapController.php              # Controller utama SAP Sync
  Console/
    Commands/
      SapDailySync.php                  # Cron job harian memanggil RptsapController

resources/
  views/
    report/
      rptsap/
        filter.blade.php                # Filter: pilih tahun + endpoint SAP
        status.blade.php                # Detail status + data preview per sync
        status_list.blade.php           # Daftar semua history sync

routes/
  web.php                              # Route group /rptsap/status dan API-nya

database/
  migrations/
    2026_08_03_105955_add_month_to_rpt_sync_status_table.php  # Menambah kolom sync_month
  seeders/
    dmenu_rptsap.php                    # Menu SAP Sync di sys_dmenu
    data_sys_auth_rptsap.php            # Autorisasi report untuk role terkait
```

---

## Routes

| Method | URL | Controller@Method | Name |
|---|---|---|---|
| GET | `/rptsap` | via `PageController` -> `RptsapController@index` | (dynamic) |
| POST | `/rptsap` | `RptsapController@store` | `rptsap.store` |
| GET | `/rptsap/status` | `RptsapController@status` | `rptsap.status` |
| GET | `/rptsap/status/{id}` | `RptsapController@status` | `rptsap.status.detail` |
| GET | `/rptsap/api/status` | `RptsapController@apiStatus` | `rptsap.api.status` |
| GET | `/rptsap/api/year-list` | `RptsapController@apiYearList` | `rptsap.year.list` |
| GET | `/rptsap/api/sync-list` | `RptsapController@apiSyncList` | `rptsap.api.status.list` |

---

## Alur Sync

### 1. Filter & Execute (Manual)

1. User membuka menu **Report > SAP Sync**.
2. Mencentang satu atau lebih endpoint SAP.
3. Memilih tahun fiskal (kompatibel untuk semua endpoint).
4. Klik **Execute**.
5. Form POST langsung ke route `rptsap.store` (`/rptsap`), bukan melalui generic `PageController`, sehingga tidak terjadi page-not-found karena view `result` tidak ditemukan. `RptsapController::store()` memvalidasi filter (hanya tahun) dan memanggil `runYearlySync()` tanpa parameter bulan.
6. `runYearlySync()` membuat record `rpt_sync_status` per endpoint (dengan `sync_month` = `null` untuk year sync).
7. Job yang didispatch akan melakukan loop bulan Januari → current month untuk `Z_GET_ONHAND`, `Z_GET_DOBYSO` dan `Z_GET_NEW_CUST` sesuai tahunnya; endpoint lain menggunakan filter tahunan seperti sebelumnya.
8. User diarahkan ke halaman **Sync History** (`rptsap.status`).

### 2. Sync Harian (Cron)

Command `php artisan sap:daily-sync` memanggil `RptsapController::runYearlySync(current_year, 'system', [], current_month)` untuk menjalankan semua endpoint secara otomatis. Untuk `Z_GET_ONHAND` dan `Z_GET_DOBYSO`, cron hanya mengambil data **bulan berjalan** (current month).

Opsi tambahan:

```bash
php artisan sap:daily-sync --dry-run          # simulasi tanpa membuat record/job
php artisan sap:daily-sync --year=2025        # override tahun (bulan tetap bulan berjalan)
```

### 3. Pencegahan Double Sync

`runYearlySync()` akan membatalkan (mark `failed`) sync yang masih `pending`, tetapi akan **menolak** request baru jika masih ada sync dengan status `processing` untuk kombinasi yang sama:
- Endpoint selain `Z_GET_ONHAND`, `Z_GET_DOBYSO` dan `Z_GET_NEW_CUST`: per `report_type` + `sync_year`.
- `Z_GET_ONHAND` / `Z_GET_DOBYSO` / `Z_GET_NEW_CUST`: per `report_type` + `sync_year`. Untuk year sync (`sync_month` = `null`), satu sync per tahun akan berjalan; untuk single-month sync (cron) masih menggunakan `sync_month` agar tidak bentrok dengan year sync.

---

## Job Dispatch Mapping

| Report Type | Job Class | Parameter Dispatch |
|---|---|---|
| `ZVBILLING` | `SapBillingSyncJob` | `($syncId, $year, $fromDate, $toDate, $requestedBy)` |
| `ZFFINANCE_YIELD` | `SapFinanceYieldSyncJob` | `($syncId, $year, $requestedBy)` |
| `Z_GET_NEW_CUST` | `SapCustomerSyncJob` | `($syncId, $year, $requestedBy)` |
| `Z_GET_PAYMENT` | `SapPaymentSyncJob` | `($syncId, $year, $requestedBy)` |
| `Z_GET_ONHAND` | `SapOnHandSyncJob` | `($syncId, $year, $requestedBy, $month)` |
| `Z_GET_DOBYSO` | `SapDoBySoSyncJob` | `($syncId, $year, $requestedBy, $month)` |
| `Z_GET_TCURR` | `SapTCurrSyncJob` | `($syncId, $year, $requestedBy)` |
| `Z_GET_SOBYRANGE` | `SapSoByRangeSyncJob` | `($syncId, $year, $fromDate, $toDate, $requestedBy)` |

- `SapOnHandSyncJob` / `SapDoBySoSyncJob`: jika `$month` bernilai `null` maka akan melakukan year sync (loop Januari → current month); jika `$month` diisi maka hanya mengambil bulan tersebut.
- `SapCustomerSyncJob`: selalu melakukan year sync (loop Januari → current month).
- `SapTCurrSyncJob`: satu panggilan per tahun dengan parameter `IV_YEAR` (endpoint `Z_GET_TCURR`). Output `ET_TCURR_DATA` berisi kolom `KURST`, `FCURR`, `TCURR`, `GDATU`, `UKURS` yang disimpan ke `rpt_et_gettcurr` (dahulu bernama `rpt_et_tcurr`, di-rename via migration `2026_08_06_130000_rename_rpt_et_tcurr_to_rpt_et_gettcurr`). Kolom `GDATU` disimpan sebagai string karena SAP mengirim **reverse date** (`99999999 - YYYYMMDD`, contoh: tanggal `2026-08-14` → `20260814` → `79739185`; per `prompts/prompt14.txt`). Reverse date terkecil = tanggal berlaku terbaru; preview tabel menampilkan konversi `d-m-Y` via `RptsapController::formatGdatu`.

---

## Menu & Autorisasi

### `sys_dmenu`

| Kolom | Nilai |
|---|---|
| `dmenu` | `rptsap` |
| `gmenu` | `report` |
| `urut` | `3` |
| `name` | `SAP Sync` |
| `url` | `rptsap` |
| `layout` | `manual` |
| `tabel` | `rpt_sync_status` |

### `sys_auth`

Role yang diberikan akses:

- `admhm1`
- `admind`
- `admins`
- `manhm1`
- `manind`
- `shhm12`
- `shind1`

---

## Catatan Penting

- `rptptr` sudah **tidak lagi** memiliki tombol Execute/History; hanya menampilkan hasil ZVBILLING yang sudah tersedia.
- Semua status sync SAP dilihat dari menu **SAP Sync**.
- `rpt_sync_status` tetap menjadi sumber utama tracking progress semua endpoint.

## Changelog

| Date | Author | Changes |
|------|--------|---------|
| 2026-08-03 | AI Agent | Initial implementation: separate SAP sync report `rptsap`, endpoint selection, sync status tracking, cron integration. |
| 2026-08-03 | AI Agent | Per `prompt_111903aug2026`: semua report di `rptsap` menjadi filter tahun. `Z_GET_ONHAND` dan `Z_GET_NEW_CUST` melakukan loop Januari → current month. Field bulan dihapus dari UI. Form action diubah ke `request()->url()` agar tidak page not found saat submit. Link detail status diperbaiki ke `rptsap.status.detail`. Job mapping & dokumentasi diperbarui. |
| 2026-08-03 | AI Agent | Menambahkan explicit `Route::post('/rptsap', ...)` ke `RptsapController@store` agar eksekusi sync tidak memerlukan view `report.rptsap.result` dan menghindari 404. Memperbaiki variabel `$selectedTypes` yang belum didefinisikan di `store()`. Verifikasi Playwright: Sales On Hand tahun 2026 berhasil sync 6.022 records Januari–Agustus. |
| 2026-08-06 | AI Agent | Per `prompts/prompt2.txt`: tambah endpoint **DO by SO (`Z_GET_DOBYSO`)** → tabel baru `rpt_et_getdobyso` + job `SapDoBySoSyncJob` (konsep pull sama dengan `ZFM_RF_GET_ONHAND`, param `IP_YEAR`/`IP_MONTH`). Diintegrasikan ke `RptsapController` (REPORT_TYPES, monthly types, dispatch, data preview), `DispatchPendingSync`, `SapDailySync`, dan view `rptsap` (label + preview). **Belum bisa di-test karena function belum rilis di SAP** (per prompt2.txt). Verifikasi: `php -l` pass, migration jalan, `view:cache` pass, test dispatch (tinker) membuat record sync `Z_GET_DOBYSO` + job ter-queue, cleanup bersih. Detail: `docs/rptptr_getdobyso.md`. |
| 2026-08-06 | AI Agent | Per `prompts/prompt12.txt`: tambah endpoint **Exchange Rate (`Z_GET_TCURR`)** → tabel baru `rpt_et_tcurr` (KURST, FCURR, TCURR, GDATU, UKURS, FISYR) + job `SapTCurrSyncJob` (satu panggilan per tahun via param `IV_YEAR`, tanpa loop bulan). Diintegrasikan ke `RptsapController` (REPORT_TYPES, dispatch, data preview), `DispatchPendingSync`, dan view `rptsap` (label + preview table). **Belum bisa di-test terhadap API karena data SAP masih belum finish** (per prompt12.txt). Verifikasi: `php -l` pass, migration jalan, `view:cache` pass, test dispatch (tinker) membuat record sync `Z_GET_TCURR` + job `SapTCurrSyncJob` ter-queue, cleanup bersih. |
| 2026-08-06 | AI Agent | Per `prompts/prompt13.txt`: tabel `rpt_et_tcurr` di-rename menjadi **`rpt_et_gettcurr`** (migration `2026_08_06_130000_rename_rpt_et_tcurr_to_rpt_et_gettcurr`); referensi di `SapTCurrSyncJob` & `RptsapController` diperbarui. Data kurs `rpt_et_gettcurr` diterapkan sebagai sumber konversi mata uang → USD untuk data **post-cutoff** di `DashboardController` (AMOUNT SO (USD) dari `trs_header_data.GRANDTOTAL` + `CURRENCY`, selalu konversi ke USD; rate terbaru per pasangan FCURR/TCURR; dukung dua arah kali/bagi) dan `RptslsController` (period & per-customer YTD/pareto). Jika kurs tidak ditemukan → fallback GRANDTOTAL mentah + alert warning "sync SAP exchange rate" (class `alert-currency`, dikecualikan dari auto-close 2,5 detik di `layouts/app.blade.php`). Verifikasi: `php -l` pass, migration jalan, tinker konversi (no-rates fallback + with-rates) benar, e2e dashboard Gwen & rptsls render tanpa error, alert warning tampil. |
| 2026-08-06 | AI Agent | Per `prompts/prompt14.txt`: kolom `GDATU` pada `rpt_et_gettcurr` ternyata **reverse date** dari SAP (`99999999 - YYYYMMDD`, contoh `79739185` = `2026-08-14`), bukan `0000-00-00`. Komentar migration create diperbarui; preview `Z_GET_TCURR` di `RptsapController` kini memakai `orderBy GDATU desc` (reverse desc = tanggal berlaku naik) dan menampilkan tanggal konversi `d-m-Y` via helper `formatGdatu` (view `status.blade.php` memakai `gdatu_display`). Verifikasi: `php -l` pass, `view:cache` pass, tinker konversi reverse date benar (79739185 → 14-08-2026, 79898768 → 31-12-2010). |
| 2026-08-06 | AI Agent | Per request user (as-of date): **konversi post-cutoff di DashboardController & RptslsController memakai rate as-of `trs_header_data.DOCUMENTDATE`** (prompt15), bukan rate terbaru global. Konsumen `rpt_et_gettcurr` berubah dari map flat `"FCURR|TCURR" => UKURS` menjadi **deret riwayat** per pasangan terurut `GDATU asc` + pemilihan via `pickAsOfRate` (rate pertama dengan `GDATU >= 99999999 - YYYYMMDD(DOCUMENTDATE)`); detail di `docs/sales_dashboard.md`. Verifikasi: `php -l` pass, `view:cache` pass, tinker as-of benar (dokumen 2026-07-29 memakai rate 2026-07-29, dokumen di luar rentang → rate terlama, pasangan tak ada → fallback+flag). |
| 2026-09-01 | AI Agent | Auto-check all endpoint checkboxes dan checkbox "Pilih Semua Endpoint" pada first load halaman filter (`resources/views/report/rptsap/filter.blade.php`). |
| 2026-09-01 | AI Agent | Refactor available data list di `filter.blade.php`: hapus kolom status & auto refresh AJAX, action View mengarahkan ke `/rptsap/status?year={year}`. Tambahkan kolom Action dengan tombol View pada `status_list.blade.php` yang mengarah ke detail sync `/rptsap/status/{id}`. |
| 2026-09-01 | AI Agent | Set default year select pada `filter.blade.php` ke current year (`date('Y')`). |
| 2026-09-01 | AI Agent | Tambah route POST `/rptsap/sync-current-year` & method `syncCurrentYear()` di `RptsapController` untuk sync SAP current year dari dashboard (`pages/dashboard/list.blade.php`), tombol hanya tampil jika role user memiliki hak akses ke `rptsap` di `sys_auth`. |
| 2026-09-07 | AI Agent | Per `prompts/prompt28.txt`: tambah endpoint **SO by Range (`Z_GET_SOBYRANGE`)** dari SAP `ZFM_SALESORDERGETRANGE` (param `IV_DATE_FROM`/`IV_DATE_TO`) &rarr; tabel baru `rpt_et_getsobyrange` (SALESDOCUMENT, SOLDTO, CREATEDBY, CREATEDDATE, FISYR) via migration `2026_09_07_110000_create_rpt_et_getsobyrange_table.php` + job baru `SapSoByRangeSyncJob`. Terintegrasi di `RptsapController` (REPORT_TYPES, dispatch yearly sync, getDataFromDatabase preview), `DispatchPendingSync`, dan preview table di `resources/views/report/rptsap/status.blade.php`. Verifikasi: `php -l` pass, migration ran, `view:cache` pass, test fetch SAP 8 records contoh prompt (2021-09-01 s.d. 2021-09-03) berhasil tersimpan dan preview normal. |
| 2026-09-10 | AI Agent | Per `prompts/prompt33.txt`: tambah kolom `ENDUSER` pada `rpt_et_getsobyrange` via migration `2026_09_10_100000_add_enduser_to_rpt_et_getsobyrange_table.php`. Update `SapSoByRangeSyncJob` untuk mapping response API SAP `ENDUSER`, update preview tabel `resources/views/report/rptsap/status.blade.php` (kolom End User) dan label `status_list.blade.php`. Verifikasi: `php -l` pass, migration jalan, `view:cache` pass, test job insert & select `ENDUSER` berhasil. |
