<?php

/**
 * PurchaseInvoiceInstallment
 *
 * Digunakan oleh CRON untuk membuat Purchase Invoice (PI) cicilan aset
 * secara otomatis setiap bulan berdasarkan kontrak yang aktif.
 *
 * Endpoint utama:
 *   POST /scheduler/PurchaseInvoiceInstallment/GenerateMonthlyInvoices
 *   POST /scheduler/PurchaseInvoiceInstallment/CurlGenerateMonthlyInvoices
 */
class PurchaseInvoiceInstallment extends MY_Controller
{
    var $db;
    var $baseUrl = "https://accone.aplikasi.web.id/one-api/";
    public function index()
    {
        echo "Purchase Invoice Installment — Auto Generate PI Cicilan Aset";
    }

    public function __construct()
    {
        parent::__construct();
    }

    /**
     * GenerateMonthlyInvoices
     *
     * Membuat Purchase Invoice cicilan untuk semua kontrak aset yang aktif
     * dan belum lunas pada bulan yang ditentukan.
     *
     * Parameter (POST JSON):
     *   - startDate  : Tanggal awal periode (format YYYY-MM-DD). Default: awal bulan ini.
     *   - endDate    : Tanggal akhir periode (format YYYY-MM-DD). Default: akhir bulan ini.
     * userID diambil otomatis dari token (sys_user["M_UserID"]).
     * Jika CRON berjalan tanpa token, fallback ke user ID 0.
     *
     * Syarat kontrak diproses:
     *   1. Kontrak aktif dan berstatus "belum lunas"
     *   2. Nilai cicilan > 0
     *   3. Jumlah cicilan terbayar < total cicilan
     *   4. Tanggal kontrak masuk dalam bulan yang dituju
     *   5. RO sudah confirmed
     *   6. PO sudah berstatus Approved
     */
    public function GenerateMonthlyInvoices()
    {
        try {
            $para   = $this->sys_input;
            // Ambil userID dari token JWT (sys_user). Fallback ke 0 jika CRON berjalan tanpa token.
            $userID = !empty($this->sys_user["M_UserID"]) ? (int) $this->sys_user["M_UserID"] : 0;
            $user   = $this->getCronUser($userID);

            $startDate = isset($para["startDate"]) && $para["startDate"] != ""
                ? $para["startDate"]
                : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-01", strtotime($para["date"])) : date("Y-m-01"));
            $endDate = isset($para["endDate"]) && $para["endDate"] != ""
                ? $para["endDate"]
                : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-t", strtotime($para["date"])) : date("Y-m-t"));
            // Validasi format tanggal
            if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) {
                throw new Exception("Format tanggal tidak valid. Gunakan format YYYY-MM-DD, contoh: 2025-07-01");
            }

            if (strtotime($startDate) > strtotime($endDate)) {
                throw new Exception("startDate tidak boleh lebih besar dari endDate");
            }

            // Tentukan rentang bulan berdasarkan tanggal acuan
            $monthStart  = $startDate;
            $monthEnd    = $endDate;
            $dayOfMonth  = (int) date("d", strtotime($endDate));

            // -------------------------------------------------------------------------
            // Ambil semua kontrak cicilan yang memenuhi syarat pada bulan ini
            // -------------------------------------------------------------------------
            $sqlKontrak = "SELECT
                    c.PurchaseOrderAssetContractID,
                    c.PurchaseOrderAssetContractPurchaseOrderID,
                    ro.ReceiveOrderPoID AS PurchaseOrderAssetContractReceiveOrderPoID,
                    c.PurchaseOrderAssetContractName,
                    c.PurchaseOrderAssetContractStartDate,
                    c.PurchaseOrderAssetContractEndDate,
                    c.PurchaseOrderAssetContractInstallmentNumber,
                    c.PurchaseOrderAssetContractInstallmentPaid,
                    c.PurchaseOrderAssetContractInstallmentDate,
                    c.PurchaseOrderAssetContractInstallmentPayAmount,
                    c.PurchaseOrderAssetContractCreatedUserID,
                    po.PurchaseOrderID,
                    po.PurchaseOrderNumber,
                    po.PurchaseOrderSupplierID,
                    po.PurchaseOrderPaymentTerm,
                    po.PurchaseOrderWarehouseType,
                    po.PurchaseOrderWarehouseID,
                    ro.ReceiveOrderPoConfirmed,
                    ps.PurchaseOrderSummaryID,
                    ps.PurchaseOrderSummaryItemID,
                    ps.PurchaseOrderSummaryItemUnitID
                FROM purchase_order_asset_contract c

                -- Pastikan PO sudah Approved dan aktif
                JOIN purchase_order po
                    ON po.PurchaseOrderID = c.PurchaseOrderAssetContractPurchaseOrderID
                    AND po.PurchaseOrderIsActive = 'Y'
                    AND po.PurchaseOrderStatus   = 'Approved'

                JOIN (
                    SELECT
                        rd.ReceiveOrderPoDetailPurchaseOrderID,
                        MIN(ro0.ReceiveOrderPoID) AS ReceiveOrderPoID,
                        MAX(ro0.ReceiveOrderPoConfirmed) AS ReceiveOrderPoConfirmed
                    FROM receive_order_po ro0
                    JOIN receive_order_po_detail rd
                        ON rd.ReceiveOrderPoDetailReceiveOrderPoID = ro0.ReceiveOrderPoID
                        AND rd.ReceiveOrderPoDetailIsActive = 'Y'
                    WHERE ro0.ReceiveOrderPoIsActive = 'Y'
                        AND ro0.ReceiveOrderPoConfirmed = 'Y'
                    GROUP BY rd.ReceiveOrderPoDetailPurchaseOrderID
                ) ro
                    ON ro.ReceiveOrderPoDetailPurchaseOrderID = po.PurchaseOrderID

                -- Ambil 1 item PO pertama sebagai referensi baris detail PI
                LEFT JOIN (
                    SELECT ps0.*
                    FROM purchase_order_summary ps0
                    JOIN (
                        SELECT
                            PurchaseOrderSummaryPurchaseOrderID,
                            MIN(PurchaseOrderSummaryID) AS PurchaseOrderSummaryID
                        FROM purchase_order_summary
                        WHERE PurchaseOrderSummaryIsActive = 'Y'
                        GROUP BY PurchaseOrderSummaryPurchaseOrderID
                    ) psx
                        ON psx.PurchaseOrderSummaryID = ps0.PurchaseOrderSummaryID
                ) ps
                    ON ps.PurchaseOrderSummaryPurchaseOrderID = po.PurchaseOrderID

                WHERE c.PurchaseOrderAssetContractIsActive = 'Y'
                    AND c.PurchaseOrderAssetContractStatus = 'belum lunas'
                    -- Hanya kontrak yang ada nilai cicilannya
                    AND IFNULL(c.PurchaseOrderAssetContractInstallmentPayAmount, 0) > 0
                    -- Hanya kontrak yang belum selesai seluruh cicilannya
                    AND IFNULL(c.PurchaseOrderAssetContractInstallmentPaid, 0) < IFNULL(c.PurchaseOrderAssetContractInstallmentNumber, 0)
                    -- Kontrak sudah mulai sebelum atau pada akhir bulan ini
                    AND DATE(c.PurchaseOrderAssetContractStartDate) <= DATE(?)
                    -- Kontrak belum berakhir (atau tidak ada tanggal akhir)
                    AND (
                        c.PurchaseOrderAssetContractEndDate IS NULL
                        OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?)
                    )
                    -- Tanggal jatuh tempo cicilan sudah melewati atau sama dengan hari ini
                    AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?";

            $params = [$monthEnd, $monthStart, $dayOfMonth];

            $qryKontrak = $this->db->query($sqlKontrak, $params);
            if (!$qryKontrak) {
                $this->sys_error_db("Gagal mengambil daftar kontrak cicilan dari database.");
                exit;
            }

            $berhasil = []; // PI yang berhasil dibuat
            $dilewati = []; // PI yang dilewati beserta alasannya

            // -------------------------------------------------------------------------
            // Proses tiap kontrak satu per satu
            // -------------------------------------------------------------------------
            $kontraks = $qryKontrak->result_array();
            foreach ($kontraks as $kontrak) {

                // Lewati jika item/satuan PO tidak ditemukan (tidak bisa buat baris detail)
                if (empty($kontrak["PurchaseOrderSummaryItemID"]) || empty($kontrak["PurchaseOrderSummaryItemUnitID"])) {
                    $dilewati[] = [
                        "kontrakID"      => $kontrak["PurchaseOrderAssetContractID"],
                        "purchaseOrderID" => $kontrak["PurchaseOrderID"],
                        "alasan"         => "Item atau satuan pada Purchase Order tidak ditemukan, tidak bisa membuat baris detail PI."
                    ];
                    continue;
                }

                // -------------------------------------------------------
                // Cek apakah PI cicilan bulan ini sudah pernah dibuat
                // -------------------------------------------------------
                $sqlCekDuplikat = "SELECT SupplierInvoiceID, SupplierInvoiceNumber
                    FROM supplier_invoice
                    WHERE SupplierInvoiceIsActive        = 'Y'
                        AND SupplierInvoiceDate           >= DATE(?)
                        AND SupplierInvoiceDate           <= DATE(?)
                        AND SupplierInvoiceStatus          = 'Draft'
                        AND (
                            SupplierInvoiceReceiveOrderPoID = ?
                            OR EXISTS (
                                SELECT 1
                                FROM supplier_invoice_detail sid
                                WHERE sid.SupplierInvoiceDetailSupplierInvoiceID = SupplierInvoiceID
                                    AND sid.SupplierInvoiceDetailReceiveOrderPoID = ?
                                    AND sid.SupplierInvoiceDetailIsActive = 'Y'
                            )
                        )
                    LIMIT 1";

                $qryCekDuplikat = $this->db->query($sqlCekDuplikat, [
                    $monthStart,
                    $monthEnd,
                    $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"],
                    $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"]
                ]);

                if (!$qryCekDuplikat) {
                    $this->sys_error_db("Gagal memeriksa duplikasi PI cicilan untuk kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . ".");
                    exit;
                }

                if ($qryCekDuplikat->num_rows() > 0) {
                    $piExisting = $qryCekDuplikat->row_array();
                    $dilewati[] = [
                        "kontrakID"             => $kontrak["PurchaseOrderAssetContractID"],
                        "purchaseOrderID"        => $kontrak["PurchaseOrderID"],
                        "receiveOrderPoID"       => $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"],
                        "supplierInvoiceID"      => $piExisting["SupplierInvoiceID"],
                        "nomorInvoice"           => $piExisting["SupplierInvoiceNumber"],
                        "alasan"                 => "PI cicilan untuk bulan ini sudah dibuat sebelumnya, tidak perlu dibuat ulang."
                    ];
                    continue;
                }

                $contractUserID = (int) ($kontrak["PurchaseOrderAssetContractCreatedUserID"] ?? 0);
                $contractUser   = $this->getCronUser($contractUserID);

                // Generate nomor PI otomatis via stored function fn_penomoran
                $nomorPI = $this->generateNomorPI($kontrak, $contractUser);

                $jumlahCicilan = (float) $kontrak["PurchaseOrderAssetContractInstallmentPayAmount"];
                $paymentTerm = isset($kontrak["PurchaseOrderPaymentTerm"]) && $kontrak["PurchaseOrderPaymentTerm"] !== null
                    ? (int) $kontrak["PurchaseOrderPaymentTerm"]
                    : 0;
                $tanggalJatuhTempo = $this->hitungJatuhTempo($endDate, $paymentTerm);
                $catatan = "PI Cicilan Otomatis — Kontrak Aset ID " . $kontrak["PurchaseOrderAssetContractID"] . " periode " . date("Y-m", strtotime($startDate));

                // -------------------------------------------------------
                // INSERT header + detail Purchase Invoice (supplier_invoice & supplier_invoice_detail)
                // -------------------------------------------------------
                $deskripsi = $kontrak["PurchaseOrderAssetContractName"] != ""
                    ? $kontrak["PurchaseOrderAssetContractName"]
                    : "Cicilan Kontrak Aset ID " . $kontrak["PurchaseOrderAssetContractID"];
                $deskripsi .= " periode " . date("Y-m", strtotime($startDate));

                $payloadInsert = [
                    "nomorPI" => $nomorPI,
                    "tanggalPI" => $endDate,
                    "tanggalJatuhTempo" => $tanggalJatuhTempo,
                    "jumlahCicilan" => $jumlahCicilan,
                    "catatan" => $catatan,
                    "deskripsi" => $deskripsi,
                    "userID" => $contractUserID,
                    "purchaseOrderAssetContractID" => $kontrak["PurchaseOrderAssetContractID"],
                    "purchaseOrderID" => $kontrak["PurchaseOrderID"],
                    "receiveOrderPoID" => $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"],
                    "supplierID" => $kontrak["PurchaseOrderSupplierID"],
                    "purchaseOrderSummaryID" => $kontrak["PurchaseOrderSummaryID"],
                    "purchaseOrderSummaryItemID" => $kontrak["PurchaseOrderSummaryItemID"],
                    "purchaseOrderSummaryItemUnitID" => $kontrak["PurchaseOrderSummaryItemUnitID"]
                ];

                $hasilInsert = $this->curlInsertSupplierInvoice($payloadInsert);
                if ($hasilInsert === false) {
                    $this->sys_error_db("Gagal menyimpan Purchase Invoice cicilan untuk kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . ".");
                    exit;
                }

                if (!empty($hasilInsert["duplicate"])) {
                    $dilewati[] = [
                        "kontrakID"             => $kontrak["PurchaseOrderAssetContractID"],
                        "purchaseOrderID"        => $kontrak["PurchaseOrderID"],
                        "receiveOrderPoID"       => $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"],
                        "supplierInvoiceID"      => $hasilInsert["supplierInvoiceID"],
                        "nomorInvoice"           => $hasilInsert["supplierInvoiceNumber"],
                        "alasan"                 => "PI cicilan untuk bulan ini sudah dibuat sebelumnya, tidak perlu dibuat ulang."
                    ];
                    continue;
                }

                $supplierInvoiceID = $hasilInsert["supplierInvoiceID"];

                // -------------------------------------------------------
                // UPDATE jumlah cicilan terbayar pada kontrak
                // -------------------------------------------------------
                $this->db->trans_begin();

                $cicilanTerbayarBaru = ((int) $kontrak["PurchaseOrderAssetContractInstallmentPaid"]) + 1;
                $statusKontrakBaru   = $cicilanTerbayarBaru >= (int) $kontrak["PurchaseOrderAssetContractInstallmentNumber"]
                    ? "lunas"
                    : "belum lunas";

                $sqlUpdateKontrak = "UPDATE purchase_order_asset_contract
                    SET PurchaseOrderAssetContractReceiveOrderPoID = ?,
                        PurchaseOrderAssetContractInstallmentPaid = ?,
                        PurchaseOrderAssetContractStatus          = ?,
                        PurchaseOrderAssetContractLastUpdated     = NOW()
                    WHERE PurchaseOrderAssetContractID    = ?
                        AND PurchaseOrderAssetContractIsActive = 'Y'";

                $qryUpdateKontrak = $this->db->query($sqlUpdateKontrak, [
                    $kontrak["PurchaseOrderAssetContractReceiveOrderPoID"],
                    $cicilanTerbayarBaru,
                    $statusKontrakBaru,
                    $kontrak["PurchaseOrderAssetContractID"]
                ]);

                if (!$qryUpdateKontrak) {
                    $this->db->trans_rollback();
                    $this->sys_error_db("Gagal memperbarui data cicilan terbayar pada kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . ".");
                    exit;
                }

                // Pastikan tidak ada error di dalam transaksi sebelum commit
                if ($this->db->trans_status() === false) {
                    $this->db->trans_rollback();
                    $this->sys_error_db("Transaksi database gagal saat memproses kontrak ID " . $kontrak["PurchaseOrderAssetContractID"] . ". Semua perubahan dibatalkan.");
                    exit;
                }

                $this->db->trans_commit();

                // Catat PI yang berhasil dibuat
                $berhasil[] = [
                    "kontrakID"      => $kontrak["PurchaseOrderAssetContractID"],
                    "purchaseOrderID" => $kontrak["PurchaseOrderID"],
                    "supplierInvoiceID" => $supplierInvoiceID,
                    "nomorInvoice"   => $nomorPI,
                    "jumlahCicilan"  => $jumlahCicilan
                ];
            }

            // -------------------------------------------------------
            // Response sukses — ringkasan hasil proses
            // -------------------------------------------------------
            $this->sys_ok([
                "startDate"      => $startDate,
                "endDate"        => $endDate,
                "periodeAwal"    => $monthStart,
                "periodeAkhir"   => $monthEnd,
                "totalDibuat"    => count($berhasil),
                "totalDilewati"  => count($dilewati),
                "daftarDibuat"   => $berhasil,
                "daftarDilewati" => $dilewati
            ]);
        } catch (Exception $exc) {
            if ($this->db->trans_status() === false) {
                $this->db->trans_rollback();
            }
            $this->sys_error($exc->getMessage());
        }
    }

    /**
     * Kirim payload insert supplier_invoice ke controller terpisah via cURL.
     *
     * @param array $payload
     * @return array|false
     */
    private function curlInsertSupplierInvoice($payload)
    {
        $endpoint = rtrim($this->baseUrl, "/") . "/mockup/scheduler/PurchaseInvoiceInstallmentInsert/InsertSupplierInvoice";

        $ch = curl_init($endpoint);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_HTTPHEADER     => [
                "Content-Type: application/json",
                "Accept: application/json"
            ],
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_CONNECTTIMEOUT => 15,
            CURLOPT_TIMEOUT        => 120
        ]);

        $response = curl_exec($ch);
        if (curl_errno($ch)) {
            $pesanError = curl_error($ch);
            curl_close($ch);
            $this->sys_error("Gagal menghubungi endpoint insert supplier_invoice. Detail: " . $pesanError);
            return false;
        }

        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode < 200 || $httpCode >= 300) {
            $this->sys_error("Endpoint insert supplier_invoice mengembalikan HTTP " . $httpCode . ".");
            return false;
        }

        $decoded = json_decode($response, true);
        if (!is_array($decoded)) {
            $this->sys_error("Response insert supplier_invoice tidak valid JSON.");
            return false;
        }

        if (!isset($decoded["status"]) || strtoupper($decoded["status"]) !== "OK") {
            $pesan = isset($decoded["message"]) ? $decoded["message"] : "Insert supplier_invoice gagal.";
            $this->sys_error($pesan);
            return false;
        }

        return isset($decoded["data"]) && is_array($decoded["data"]) ? $decoded["data"] : $decoded;
    }

    public function ListEligibleContracts()
    {
        try {
            $para = $this->sys_input;
            $startDate = isset($para["startDate"]) && $para["startDate"] != ""
                ? $para["startDate"]
                : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-01", strtotime($para["date"])) : date("Y-m-01"));
            $endDate = isset($para["endDate"]) && $para["endDate"] != ""
                ? $para["endDate"]
                : (isset($para["date"]) && $para["date"] != "" ? date("Y-m-t", strtotime($para["date"])) : date("Y-m-t"));

            if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) {
                throw new Exception("Format tanggal tidak valid. Gunakan format YYYY-MM-DD, contoh: 2025-07-01");
            }

            if (strtotime($startDate) > strtotime($endDate)) {
                throw new Exception("startDate tidak boleh lebih besar dari endDate");
            }

            $monthStart = $startDate;
            $monthEnd = $endDate;
            $dayOfMonth = (int) date("d", strtotime($endDate));

            $sqlKontrak = "SELECT
                    c.PurchaseOrderAssetContractID,
                    c.PurchaseOrderAssetContractPurchaseOrderID,
                    ro.ReceiveOrderPoID AS PurchaseOrderAssetContractReceiveOrderPoID,
                    c.PurchaseOrderAssetContractName,
                    c.PurchaseOrderAssetContractStartDate,
                    c.PurchaseOrderAssetContractEndDate,
                    c.PurchaseOrderAssetContractInstallmentNumber,
                    c.PurchaseOrderAssetContractInstallmentPaid,
                    c.PurchaseOrderAssetContractInstallmentDate,
                    c.PurchaseOrderAssetContractInstallmentPayAmount,
                    po.PurchaseOrderID,
                    po.PurchaseOrderNumber,
                    po.PurchaseOrderSupplierID,
                    po.PurchaseOrderPaymentTerm,
                    po.PurchaseOrderWarehouseType,
                    po.PurchaseOrderWarehouseID,
                    ro.ReceiveOrderPoConfirmed,
                    ps.PurchaseOrderSummaryID,
                    ps.PurchaseOrderSummaryItemID,
                    ps.PurchaseOrderSummaryItemUnitID
                FROM purchase_order_asset_contract c
                JOIN purchase_order po
                    ON po.PurchaseOrderID = c.PurchaseOrderAssetContractPurchaseOrderID
                    AND po.PurchaseOrderIsActive = 'Y'
                    AND po.PurchaseOrderStatus   = 'Approved'

                JOIN (
                    SELECT
                        rd.ReceiveOrderPoDetailPurchaseOrderID,
                        MIN(ro0.ReceiveOrderPoID) AS ReceiveOrderPoID,
                        MAX(ro0.ReceiveOrderPoConfirmed) AS ReceiveOrderPoConfirmed
                    FROM receive_order_po ro0
                    JOIN receive_order_po_detail rd
                        ON rd.ReceiveOrderPoDetailReceiveOrderPoID = ro0.ReceiveOrderPoID
                        AND rd.ReceiveOrderPoDetailIsActive = 'Y'
                    WHERE ro0.ReceiveOrderPoIsActive = 'Y'
                        AND ro0.ReceiveOrderPoConfirmed = 'Y'
                    GROUP BY rd.ReceiveOrderPoDetailPurchaseOrderID
                ) ro
                    ON ro.ReceiveOrderPoDetailPurchaseOrderID = po.PurchaseOrderID

                LEFT JOIN (
                    SELECT ps0.*
                    FROM purchase_order_summary ps0
                    JOIN (
                        SELECT
                            PurchaseOrderSummaryPurchaseOrderID,
                            MIN(PurchaseOrderSummaryID) AS PurchaseOrderSummaryID
                        FROM purchase_order_summary
                        WHERE PurchaseOrderSummaryIsActive = 'Y'
                        GROUP BY PurchaseOrderSummaryPurchaseOrderID
                    ) psx
                        ON psx.PurchaseOrderSummaryID = ps0.PurchaseOrderSummaryID
                ) ps
                    ON ps.PurchaseOrderSummaryPurchaseOrderID = po.PurchaseOrderID
                WHERE c.PurchaseOrderAssetContractIsActive = 'Y'
                    AND c.PurchaseOrderAssetContractStatus = 'belum lunas'
                    AND IFNULL(c.PurchaseOrderAssetContractInstallmentPayAmount, 0) > 0
                    AND IFNULL(c.PurchaseOrderAssetContractInstallmentPaid, 0) < IFNULL(c.PurchaseOrderAssetContractInstallmentNumber, 0)
                    AND DATE(c.PurchaseOrderAssetContractStartDate) <= DATE(?)
                    AND (
                        c.PurchaseOrderAssetContractEndDate IS NULL
                        OR DATE(c.PurchaseOrderAssetContractEndDate) >= DATE(?)
                    )
                    AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?
                ORDER BY c.PurchaseOrderAssetContractID ASC";

            $qryKontrak = $this->db->query($sqlKontrak, [$monthEnd, $monthStart, $dayOfMonth]);
            if (!$qryKontrak) {
                $this->sys_error_db("Gagal mengambil daftar kontrak cicilan eligible.");
                exit;
            }

            $this->sys_ok([
                "startDate" => $startDate,
                "endDate" => $endDate,
                "periodeAwal" => $monthStart,
                "periodeAkhir" => $monthEnd,
                "total" => $qryKontrak->num_rows(),
                "records" => $qryKontrak->result_array()
            ]);
        } catch (Exception $exc) {
            $this->sys_error($exc->getMessage());
        }
    }

    /**
     * CurlGenerateMonthlyInvoices
     *
     * Wrapper untuk memanggil GenerateMonthlyInvoices melalui HTTP request (cURL).
     * Cocok digunakan oleh CRON eksternal yang tidak bisa memanggil function PHP langsung.
     *
     * Parameter (POST JSON):
     *   - baseUrl    : Base URL server tujuan. Default: URL server ini sendiri.
     *   - startDate  : Tanggal awal periode (YYYY-MM-DD). Default: awal bulan ini.
     *   - endDate    : Tanggal akhir periode (YYYY-MM-DD). Default: akhir bulan ini.
     *   - userID     : ID user yang menjalankan. Default: 0.
     */

    // =========================================================================
    // FUNGSI PRIVATE / HELPER
    // =========================================================================

    /**
     * Generate nomor Purchase Invoice.
     *
     * @param array $kontrak Data baris kontrak dari query
     * @param array $user    Data user CRON
     * @return string        Nomor PI yang dihasilkan
     */
    private function generateNomorPI($kontrak, $user)
    {
        $userID = isset($user["M_UserID"]) ? (int) $user["M_UserID"] : 0;

        // Tentukan area ID dan type
        $areaid = isset($user["M_BranchID"]) && (int) $user["M_BranchID"] > 0 ? (int) $user["M_BranchID"] : 0;
        $areatype = 'B';
        if (isset($user["loginLevel"]) && $user["loginLevel"] == 'regional') {
            $areaid = isset($user["S_RegionalID"]) && (int) $user["S_RegionalID"] > 0 ? (int) $user["S_RegionalID"] : 0;
            $areatype = 'R';
        }

        // Ambil divisi user
        $userDivID = 0;
        if ($userID > 0) {
            $sqlusrdivisi = "SELECT M_UserDivisionDivisionID FROM m_userdivision
                WHERE M_UserDivisionM_UserID = ? AND M_UserDivisionIsActive = 'Y' LIMIT 1";
            $queusrdivisi = $this->db->query($sqlusrdivisi, [$userID]);
            if ($queusrdivisi && $queusrdivisi->num_rows() > 0) {
                $userDivID = (int) $queusrdivisi->row_array()['M_UserDivisionDivisionID'];
            }
        }

        // Jalankan stored function fn_penomoran
        $sqlnum = "SELECT `fn_penomoran`(?, ?, ?, ?, ?, ?) AS numpd;";
        $quenum = $this->db->query($sqlnum, ['PI', $userDivID, $areatype, $areaid, 'SM', 'N']);
        if ($quenum && $quenum->num_rows() > 0) {
            return $quenum->row_array()['numpd'];
        }

        // Fallback jika stored function gagal
        $contractID = isset($kontrak["PurchaseOrderAssetContractID"]) ? (int) $kontrak["PurchaseOrderAssetContractID"] : 0;
        return "PI-INS-FALLBACK-" . $contractID . "-" . date("Ymd");
    }

    /**
     * Ambil data user berdasarkan userID untuk keperluan CRON.
     * Jika user tidak ditemukan, kembalikan data default (tanpa branch/regional).
     *
     * @param int $userID
     * @return array
     */
    private function getCronUser($userID)
    {
        $sql = "SELECT
                M_UserID,
                M_UserM_BranchID   AS M_BranchID,
                M_UserS_RegionalID AS S_RegionalID
            FROM m_user
            WHERE M_UserID = ?
            LIMIT 1";

        $qry = $this->db->query($sql, [$userID]);
        if ($qry && $qry->num_rows() > 0) {
            $user = $qry->row_array();
            $user["loginLevel"] = "branch";
            return $user;
        }

        // User tidak ditemukan, gunakan data kosong agar proses tetap berjalan
        return [
            "M_UserID"     => $userID,
            "M_BranchID"   => 0,
            "S_RegionalID" => 0,
            "loginLevel"   => "branch"
        ];
    }

    /**
     * Hitung tanggal jatuh tempo berdasarkan tanggal acuan dan payment term (dalam hari).
     *
     * @param string $tanggal  Format YYYY-MM-DD
     * @param int    $term     Jumlah hari payment term
     * @return string          Tanggal jatuh tempo (YYYY-MM-DD)
     */
    private function hitungJatuhTempo($tanggal, $term)
    {
        $hari = is_numeric($term) ? (int) $term : 0;
        if ($hari < 0) {
            $hari = 0;
        }

        return date("Y-m-d", strtotime($tanggal . " +" . $hari . " days"));
    }

    /**
     * Validasi apakah string adalah tanggal yang valid dengan format YYYY-MM-DD.
     *
     * @param string $tanggal
     * @return bool
     */
    private function isValidDate($tanggal)
    {
        $d = DateTime::createFromFormat("Y-m-d", $tanggal);
        return $d && $d->format("Y-m-d") === $tanggal;
    }
}
