<?php

/**
 * JurnalPiCicilanOrderAset
 *
 * Controller scheduler untuk membuat jurnal akuntansi atas Purchase Invoice (PI)
 * cicilan order aset secara otomatis.
 *
 * Alur bisnis proses:
 *   1. Validasi parameter input (tanggal, branchCode, title, description, details)
 *   2. Insert header jurnal ke tabel `jurnal`
 *      - Nomor jurnal di-generate via fn_numbering('J')
 *      - Tipe jurnal: PAYMENT
 *      - Berisi: title, description, tanggal, regionalID, kode cabang, branchCompanyID
 *   3. Insert baris transaksi ke tabel `jurnal_tx` (untuk setiap baris cicilan)
 *      - Referensi ke jurnalID header
 *      - COA (Chart of Account) dan deskripsinya:
 *          * Sisi hutang cicilan → nilai DEBIT (diambil dari HutangCoaID di fa_class)
 *          * Sisi pembayaran cicilan → nilai KREDIT (diambil dari ItemCoaID di fa_class)
 *   4. Insert data tambahan ke tabel `jurnal_addon` (jika ada addonValue atau mItemID)
 *      - Referensi ke jurnalID dan jurnalTxID
 *      - Berisi: addonCode (default: "PIINSTALMENT"), addonValue, mItemID
 *
 * Endpoint:
 *   POST /mockup/scheduler/JurnalPiCicilanOrderAset/CreateJournal
 *   POST /mockup/scheduler/JurnalPiCicilanOrderAset/GenerateJournalsFromContracts
 *   GET  /mockup/scheduler/JurnalPiCicilanOrderAset/ListEligiblePurchaseOrderAssetContracts
 */
class JurnalPiCicilanOrderAset extends MY_Controller
{
    var $db;

    /**
     * Endpoint default — menampilkan nama controller sebagai penanda aktif.
     */
    public function index()
    {
        echo "Jurnal PI Cicilan Order Aset";
    }

    /**
     * Konstruktor — inisialisasi parent controller.
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * CreateJournal — Membuat jurnal PI cicilan order aset.
     *
     * Proses bisnis:
     *   1. Validasi semua parameter wajib: date, branchCode, title, description, details
     *   2. Ambil periodeID dari tanggal transaksi (tabel periode)
     *   3. Ambil regionalID dari branchCode (tabel m_branch)
     *   4. Ambil branchCompanyID dari branchCode (tabel m_branch_companydetail)
     *   5. Ambil jurnalTypeID dari kode tipe jurnal (default: "PAYMENT")
     *   6. Generate nomor jurnal otomatis via fn_numbering('J')
     *   7. Validasi balance: total debit harus sama dengan total credit
     *   8. INSERT header jurnal ke tabel `jurnal`:
     *      - jurnalNo         : nomor jurnal hasil generate
     *      - jurnalJurnalTypeID: tipe PAYMENT
     *      - jurnalTitle      : judul jurnal
     *      - jurnalDescription: keterangan jurnal
     *      - jurnalDate       : tanggal transaksi
     *      - JurnalS_RegionalID: ID regional cabang
     *      - jurnalM_BranchCode: kode cabang
     *   9. Untuk setiap baris detail, INSERT ke `jurnal_tx`:
     *      - jurnalTxJurnalID  : referensi ke header jurnal
     *      - jurnalTxCoaID     : COA yang digunakan (hutang cicilan / kas/bank)
     *      - jurnalTxDescription: keterangan baris (nama cicilan/aset)
     *      - jurnalTxDebit     : nilai hutang (sisi debit)
     *      - jurnalTxCredit    : nilai bayar (sisi kredit)
     *  10. Jika ada addonValue atau mItemID, INSERT ke `jurnal_addon`:
     *      - jurnalAddOnJurnalID  : referensi ke header jurnal
     *      - jurnalAddOnJurnalTxID: referensi ke baris jurnal_tx
     *      - jurnalAddOnCode      : kode addon (default: "PIINSTALMENT")
     *      - jurnalAddOnValue     : nilai tambahan (misal: nomor PI/kontrak)
     *      - jurnalAddOnM_ItemID  : ID item aset yang terkait
     *
     * Parameter (POST JSON):
     *   - date          : Tanggal jurnal (YYYY-MM-DD). Default: hari ini.
     *   - branchCode    : Kode cabang. WAJIB.
     *   - title         : Judul jurnal. WAJIB.
     *   - description   : Keterangan jurnal. WAJIB.
     *   - details       : Array baris transaksi. WAJIB, minimal 1 baris.
     *       - coaID       : ID Chart of Account. WAJIB per baris.
     *       - description : Keterangan baris.
     *       - debit       : Nilai debit (hutang cicilan).
     *       - credit      : Nilai kredit (pembayaran cicilan).
     *       - addonValue  : (opsional) Nilai addon, misal nomor PI.
     *       - mItemID     : (opsional) ID item aset.
     *       - addonCode   : (opsional) Kode addon. Default: "PIINSTALMENT".
     *   - periodeID     : (opsional) ID periode. Default: diambil dari tanggal.
     *   - regionalID    : (opsional) ID regional. Default: diambil dari branchCode.
     *   - branchCompanyID: (opsional) ID branch company. Default: diambil dari branchCode.
     *   - jurnalTypeID  : (opsional) ID tipe jurnal. Default: diambil dari jurnalTypeCode.
     *   - jurnalTypeCode: (opsional) Kode tipe jurnal. Default: "PAYMENT".
     *   - userID        : ID user yang membuat jurnal.
     *
     * @return JSON { jurnalID, jurnalNo, detailCount, addonCount }
     */
    public function CreateJournal()
    {
        try {
            $para = $this->sys_input;

            $date = isset($para["date"]) && $para["date"] != "" ? $para["date"] : date("Y-m-d");
            if (!$this->isValidDate($date)) {
                throw new Exception("Format tanggal tidak valid. Gunakan YYYY-MM-DD.");
            }

            $branchCode = isset($para["branchCode"]) ? trim($para["branchCode"]) : "";
            if ($branchCode == "") {
                throw new Exception("branchCode wajib diisi.");
            }

            $title = isset($para["title"]) ? trim($para["title"]) : "";
            if ($title == "") {
                throw new Exception("title wajib diisi.");
            }

            $description = isset($para["description"]) ? trim($para["description"]) : "";
            if ($description == "") {
                throw new Exception("description wajib diisi.");
            }

            $details = isset($para["details"]) && is_array($para["details"]) ? $para["details"] : [];
            if (count($details) == 0) {
                throw new Exception("details wajib diisi minimal 1 baris.");
            }

            $periodeID = isset($para["periodeID"]) && (int) $para["periodeID"] > 0
                ? (int) $para["periodeID"]
                : $this->getPeriodeIDByDate($date);
            if ($periodeID <= 0) {
                throw new Exception("Periode jurnal tidak ditemukan untuk tanggal {$date}.");
            }

            $regionalID = isset($para["regionalID"]) && (int) $para["regionalID"] > 0
                ? (int) $para["regionalID"]
                : $this->getRegionalIDByBranchCode($branchCode);
            if ($regionalID <= 0) {
                throw new Exception("regionalID wajib diisi atau harus bisa diturunkan dari branchCode.");
            }

            $branchCompanyID = isset($para["branchCompanyID"]) && (int) $para["branchCompanyID"] > 0
                ? (int) $para["branchCompanyID"]
                : $this->getBranchCompanyIDByBranchCode($branchCode);
            if ($branchCompanyID <= 0) {
                throw new Exception("branchCompanyID wajib diisi atau harus bisa diturunkan dari branchCode.");
            }

            $jurnalTypeID = isset($para["jurnalTypeID"]) && (int) $para["jurnalTypeID"] > 0
                ? (int) $para["jurnalTypeID"]
                : $this->getJurnalTypeID(isset($para["jurnalTypeCode"]) && $para["jurnalTypeCode"] != "" ? $para["jurnalTypeCode"] : "PAYMENT");
            if ($jurnalTypeID <= 0) {
                throw new Exception("jurnalTypeID / jurnalTypeCode tidak valid.");
            }

            $userID = 0;

            $result = $this->insertJournal(
                $date,
                $branchCode,
                $title,
                $description,
                $details,
                $periodeID,
                $regionalID,
                $branchCompanyID,
                $jurnalTypeID,
                $userID
            );

            if ($result === false) {
                exit;
            }

            $this->sys_ok($result);
        } catch (Exception $exc) {
            $this->sys_error($exc->getMessage());
        }
    }

    /**
     * GenerateJournalsFromContracts — Generate jurnal otomatis dari seluruh kontrak aset eligible.
     *
     * Fungsi ini mengintegrasikan data dari getEligiblePurchaseOrderAssetContracts()
     * dengan proses pembuatan jurnal (insertJournal()) secara otomatis.
     *
     * Untuk setiap kontrak yang eligible, dibuat 1 jurnal dengan 2 baris transaksi:
     *   - Baris DEBIT  : COA hutang cicilan (HutangCoaID dari fa_class) — mencatat hutang cicilan
     *   - Baris KREDIT : COA kas/bank/aset (ItemCoaID dari fa_class) — mencatat pembayaran cicilan
     *   Addon (jurnal_addon): menyimpan ID kontrak sebagai addonValue dan ItemID sebagai mItemID
     *
     * Parameter (POST JSON):
     *   - date       : Tanggal jurnal (YYYY-MM-DD). Default: hari ini.
     *   - userID     : ID user yang membuat jurnal.
     *   - dryRun     : "Y" = simulasi tanpa simpan ke DB. Default: "N".
     *   - contractID : (opsional) Filter hanya 1 kontrak tertentu.
     *
     * Syarat kontrak dilewati (skipped):
     *   - branchCode kosong (data cabang belum lengkap)
     *   - COA hutang (HutangCoaID) tidak ditemukan di fa_class
     *   - COA bayar/aset (ItemCoaID) tidak ditemukan di fa_class
     *
     * @return JSON { tanggal, totalDibuat, totalDilewati, daftarDibuat[], daftarDilewati[] }
     */
    public function GenerateJournalsFromContracts()
    {
        try {
            $para = $this->sys_input;

            $date = isset($para["date"]) && $para["date"] != "" ? $para["date"] : date("Y-m-d");
            if (!$this->isValidDate($date)) {
                throw new Exception("Format tanggal tidak valid. Gunakan YYYY-MM-DD.");
            }

            $userID  = 0;
            $dryRun  = isset($para["dryRun"])  && ($para["dryRun"] === true || $para["dryRun"] == "Y" || $para["dryRun"] == "1");
            $filterContractID = isset($para["contractID"]) && (int) $para["contractID"] > 0
                ? (int) $para["contractID"]
                : null;

            $monthStart = date("Y-m-01", strtotime($date));
            $monthEnd   = date("Y-m-t",  strtotime($date));

            // Ambil semua kontrak yang eligible untuk periode bulan ini
            $contracts = $this->getEligiblePurchaseOrderAssetContracts($monthStart, $monthEnd);

            $dibuat   = [];
            $dilewati = [];

            foreach ($contracts as $kontrak) {
                $contractID = (int) $kontrak["PurchaseOrderAssetContractID"];

                // Filter 1 kontrak jika diminta
                if ($filterContractID !== null && $contractID !== $filterContractID) {
                    continue;
                }

                $branchCode      = $kontrak["M_BranchCode"]      ?? "";
                $regionalID      = (int) ($kontrak["M_BranchS_RegionalID"] ?? 0);
                $branchCompanyID = (int) ($kontrak["M_BranchCompanyID"]    ?? 0);
                $coaHutangID     = (int) ($kontrak["HutangCoaID"]          ?? 0); // COA debit dari fa_class (Fa_ClassHutangCoaID)
                $coaBayarID      = (int) ($kontrak["ItemCoaID"]            ?? 0); // COA kredit dari fa_class (Fa_ClassCoaID)
                $jumlah          = (float) $kontrak["PurchaseOrderAssetContractInstallmentPayAmount"];
                $itemID          = (int) ($kontrak["PurchaseOrderSummaryItemID"] ?? 0);
                $namaKontrak     = $kontrak["PurchaseOrderAssetContractName"] != ""
                    ? $kontrak["PurchaseOrderAssetContractName"]
                    : "Kontrak ID {$contractID}";
                $nomorPO         = $kontrak["PurchaseOrderNumber"] ?? "";
                $cicilanKe       = ((int) $kontrak["PurchaseOrderAssetContractInstallmentPaid"]) + 1;
                $totalCicilan    = (int) $kontrak["PurchaseOrderAssetContractInstallmentNumber"];
                $periode         = date("Y-m", strtotime($date));
                $invoiceNumber   = $kontrak["SupplierInvoiceNumber"] ?? "";
                $invoiceID       = (int) ($kontrak["SupplierInvoiceID"] ?? 0);

                // Validasi: branchCode harus ada
                if ($branchCode == "") {
                    $dilewati[] = [
                        "kontrakID" => $contractID,
                        "alasan"    => "Kode cabang (branchCode) tidak ditemukan. Pastikan warehouse PO sudah terhubung ke data cabang."
                    ];
                    continue;
                }

                // Validasi: COA hutang harus ada (untuk sisi DEBIT)
                if ($coaHutangID <= 0) {
                    $dilewati[] = [
                        "kontrakID"  => $contractID,
                        "branchCode" => $branchCode,
                        "alasan"     => "COA hutang tidak ditemukan. Pastikan item PO memiliki Fa_Class dengan Fa_ClassHutangCoaID."
                    ];
                    continue;
                }

                // Validasi: COA bayar/aset harus ada (untuk sisi KREDIT)
                if ($coaBayarID <= 0) {
                    $dilewati[] = [
                        "kontrakID"  => $contractID,
                        "branchCode" => $branchCode,
                        "alasan"     => "COA bayar/aset tidak ditemukan. Pastikan item PO memiliki Fa_Class dengan Fa_ClassCoaID."
                    ];
                    continue;
                }

                // Mode simulasi: catat tanpa simpan ke DB
                if ($dryRun) {
                    $dibuat[] = [
                        "kontrakID"      => $contractID,
                        "branchCode"     => $branchCode,
                        "namaKontrak"    => $namaKontrak,
                        "invoiceNumber"  => $invoiceNumber,
                        "jumlah"         => $jumlah,
                        "coaHutangID"    => $coaHutangID,
                        "coaBayarID"     => $coaBayarID,
                        "keterangan"     => "Mode simulasi (dryRun), data tidak disimpan."
                    ];
                    continue;
                }

                // Susun title dan description jurnal
                $title       = "PI Cicilan Aset - {$invoiceNumber}";
                $description = "Jurnal cicilan ke-{$cicilanKe} dari {$totalCicilan} | {$namaKontrak} | PO: {$nomorPO} | Periode {$periode}";

                // Ambil periodeID dari tanggal
                $periodeID = $this->getPeriodeIDByDate($date);
                if ($periodeID <= 0) {
                    $dilewati[] = [
                        "kontrakID"  => $contractID,
                        "branchCode" => $branchCode,
                        "alasan"     => "Periode jurnal tidak ditemukan untuk tanggal {$date}."
                    ];
                    continue;
                }

                // Ambil jurnalTypeID untuk tipe PAYMENT
                $jurnalTypeID = $this->getJurnalTypeID("PAYMENTINV");
                if ($jurnalTypeID <= 0) {
                    $dilewati[] = [
                        "kontrakID"  => $contractID,
                        "branchCode" => $branchCode,
                        "alasan"     => "Tipe jurnal PAYMENT tidak ditemukan di tabel jurnal_type."
                    ];
                    continue;
                }

                // Ambil description detail dari database fa_class dengan fallback jika kosong
                $descHutang = isset($kontrak["HutangCoaDesc"]) && $kontrak["HutangCoaDesc"] != ""
                    ? $kontrak["HutangCoaDesc"]
                    : "Hutang cicilan aset - {$namaKontrak}";
                $descBayar = isset($kontrak["ItemCoaDesc"]) && $kontrak["ItemCoaDesc"] != ""
                    ? $kontrak["ItemCoaDesc"]
                    : "Pembayaran cicilan aset - {$namaKontrak}";

                // Susun 2 baris detail jurnal:
                //   Baris 1 — DEBIT  : hutang cicilan aset (COA dari fa_class)
                //   Baris 2 — KREDIT : pembayaran cicilan (COA kas/bank dari parameter)
                $details = [
                    [
                        "coaID"       => $coaHutangID,
                        "description" => $descHutang,
                        "debit"       => $jumlah,
                        "credit"      => 0,
                        "addonValue"  => $invoiceNumber,
                        "mItemID"     => $itemID,
                        "addonCode"   => "PIINSTALMENT"
                    ],
                    [
                        "coaID"       => $coaBayarID,
                        "description" => $descBayar,
                        "debit"       => 0,
                        "credit"      => $jumlah,
                        "addonValue"  => $invoiceNumber,
                        "mItemID"     => $itemID,
                        "addonCode"   => "PIINSTALMENT"
                    ]
                ];


                // Panggil insertJournal() untuk simpan ke DB
                $result = $this->insertJournal(
                    $date,
                    $branchCode,
                    $title,
                    $description,
                    $details,
                    $periodeID,
                    $regionalID,
                    $branchCompanyID,
                    $jurnalTypeID,
                    $userID
                );

                if ($result === false) {
                    $dilewati[] = [
                        "kontrakID"  => $contractID,
                        "branchCode" => $branchCode,
                        "alasan"     => "Gagal menyimpan jurnal ke database."
                    ];
                    continue;
                }

                $dibuat[] = [
                    "kontrakID"   => $contractID,
                    "branchCode"  => $branchCode,
                    "namaKontrak" => $namaKontrak,
                    "jurnalID"    => $result["jurnalID"],
                    "jurnalNo"    => $result["jurnalNo"],
                    "jumlah"      => $jumlah
                ];
            }

            $this->sys_ok([
                "tanggal"        => $date,
                "periodeAwal"    => $monthStart,
                "periodeAkhir"   => $monthEnd,
                "totalDibuat"    => count($dibuat),
                "totalDilewati"  => count($dilewati),
                "daftarDibuat"   => $dibuat,
                "daftarDilewati" => $dilewati
            ]);
        } catch (Exception $exc) {
            $this->sys_error($exc->getMessage());
        }
    }


    /**
     * ListEligiblePurchaseOrderAssetContracts — Menampilkan daftar kontrak aset
     * yang memenuhi syarat untuk dibuatkan jurnal PI cicilan pada periode tertentu.
     *
     * Syarat kontrak eligible:
     *   - Kontrak aktif dan berstatus "belum lunas"
     *   - Nilai cicilan > 0
     *   - Jumlah cicilan terbayar < total cicilan
     *   - Tanggal kontrak masuk dalam rentang startDate–endDate
     *   - PO sudah berstatus Approved
     *   - Barang sudah diterima (receive_order_po sudah confirmed)
     *
     * Endpoint ini digunakan untuk preview/verifikasi sebelum proses generate jurnal
     * dijalankan secara massal oleh CRON.
     *
     * Parameter (GET/POST):
     *   - startDate : Awal periode (YYYY-MM-DD). Default: tanggal 1 bulan ini.
     *   - endDate   : Akhir periode (YYYY-MM-DD). Default: tanggal akhir bulan ini.
     *
     * @return JSON { startDate, endDate, total, records[] }
     */
    public function ListEligiblePurchaseOrderAssetContracts()
    {
        try {
            $para = $this->sys_input;

            $startDate = isset($para["startDate"]) && $para["startDate"] != ""
                ? $para["startDate"]
                : date("Y-m-01");
            $endDate = isset($para["endDate"]) && $para["endDate"] != ""
                ? $para["endDate"]
                : date("Y-m-t");

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

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

            $records = $this->getEligiblePurchaseOrderAssetContracts($startDate, $endDate);

            $this->sys_ok([
                "startDate" => $startDate,
                "endDate"   => $endDate,
                "total"     => count($records),
                "records"   => $records
            ]);
        } catch (Exception $exc) {
            $this->sys_error($exc->getMessage());
        }
    }

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

    /**
     * insertJournal — Menyimpan 1 jurnal lengkap ke database (header + tx + addon).
     *
     * Fungsi internal yang dipanggil oleh CreateJournal() dan GenerateJournalsFromContracts().
     * Mengelola transaksi DB secara penuh (begin → commit/rollback).
     *
     * Alur:
     *   1. Validasi balance debit == kredit
     *   2. Generate nomor jurnal via fn_numbering('J')
     *   3. INSERT header ke tabel `jurnal`
     *   4. INSERT baris transaksi ke `jurnal_tx` (per detail)
     *   5. INSERT addon ke `jurnal_addon` (jika addonValue atau mItemID ada)
     *
     * @param string $date           Tanggal jurnal (YYYY-MM-DD)
     * @param string $branchCode     Kode cabang
     * @param string $title          Judul jurnal
     * @param string $description    Keterangan jurnal
     * @param array  $details        Baris transaksi [ coaID, description, debit, credit, addonValue?, mItemID?, addonCode? ]
     * @param int    $periodeID      ID periode akuntansi
     * @param int    $regionalID     ID regional
     * @param int    $branchCompanyID ID branch company
     * @param int    $jurnalTypeID   ID tipe jurnal
     * @param int    $userID         ID user pembuat jurnal
     *
     * @return array|false  Array { jurnalID, jurnalNo, detailCount, addonCount }, atau false jika gagal
     */
    private function insertJournal($date, $branchCode, $title, $description, $details, $periodeID, $regionalID, $branchCompanyID, $jurnalTypeID, $userID)
    {
        // Validasi balance sebelum menyimpan
        $totalDebit  = 0;
        $totalCredit = 0;
        foreach ($details as $idx => $detail) {
            $debit  = isset($detail["debit"])  ? (float) $detail["debit"]  : 0;
            $credit = isset($detail["credit"]) ? (float) $detail["credit"] : 0;
            if ($debit < 0 || $credit < 0) {
                $this->sys_error("Nilai debit/credit tidak boleh negatif pada detail ke-" . ($idx + 1));
                return false;
            }
            $totalDebit  += $debit;
            $totalCredit += $credit;
        }

        if (round($totalDebit, 2) !== round($totalCredit, 2)) {
            $this->sys_error("Total debit dan credit harus sama. Debit={$totalDebit}, Credit={$totalCredit}");
            return false;
        }

        $jurnalNo = $this->generateJournalNumber();
        if ($jurnalNo === false || $jurnalNo == "") {
            $this->sys_error("Gagal generate nomor jurnal.");
            return false;
        }

        $this->db->trans_begin();

        // -----------------------------------------------------------------
        // INSERT header jurnal ke tabel `jurnal`
        // -----------------------------------------------------------------
        $sqlHeader = "INSERT INTO jurnal (
                jurnalM_BranchCompanyID,
                JurnalS_RegionalID,
                jurnalM_BranchCode,
                jurnalperiodeID,
                jurnalNo,
                jurnalTitle,
                jurnalDescription,
                jurnalDate,
                jurnalJurnalTypeID,
                jurnalCreated,
                jurnalM_UserID
            ) VALUES (?,?,?,?,?,?,?,?,?,NOW(),?)";

        $qryHeader = $this->db->query($sqlHeader, [
            $branchCompanyID,
            $regionalID,
            $branchCode,
            $periodeID,
            $jurnalNo,
            $title,
            $description,
            $date,
            $jurnalTypeID,
            $userID
        ]);
        if (!$qryHeader) {
            $this->db->trans_rollback();
            $this->sys_error_db("Gagal insert jurnal header.");
            return false;
        }

        $jurnalID         = $this->db->insert_id();
        $createdAddonCount = 0;

        // -----------------------------------------------------------------
        // INSERT baris transaksi ke tabel `jurnal_tx` (per baris cicilan)
        // -----------------------------------------------------------------
        foreach ($details as $idx => $detail) {
            $coaID         = isset($detail["coaID"])       ? (int)   $detail["coaID"]    : 0;
            $descriptionTx = isset($detail["description"]) ? trim($detail["description"]) : "";
            $debit         = isset($detail["debit"])       ? (float) $detail["debit"]     : 0;
            $credit        = isset($detail["credit"])      ? (float) $detail["credit"]    : 0;

            if ($coaID <= 0) {
                $this->db->trans_rollback();
                $this->sys_error_db("coaID wajib diisi pada detail ke-" . ($idx + 1));
                return false;
            }

            // Sisi debit  = hutang cicilan (COA hutang aset dari fa_class)
            // Sisi kredit = pembayaran cicilan (COA kas/bank)
            $sqlTx = "INSERT INTO jurnal_tx (
                    jurnalTxJurnalID,
                    jurnalTxCoaID,
                    jurnalTxDescription,
                    jurnalTxDebit,
                    jurnalTxCredit,
                    jurnalTxCreated,
                    jurnalTxM_UserID
                ) VALUES (?,?,?,?,?,NOW(),?)";

            $qryTx = $this->db->query($sqlTx, [
                $jurnalID,
                $coaID,
                $descriptionTx,
                $debit,
                $credit,
                $userID
            ]);
            if (!$qryTx) {
                $this->db->trans_rollback();
                $this->sys_error_db("Gagal insert jurnal_tx pada detail ke-" . ($idx + 1));
                return false;
            }

            $jurnalTxID = $this->db->insert_id();

            // -----------------------------------------------------------------
            // INSERT addon ke `jurnal_addon` (jika addonValue atau mItemID ada)
            // addonValue  = ID kontrak aset (untuk tracing)
            // mItemID     = ID item aset dari purchase_order_summary
            // -----------------------------------------------------------------
            $addonValue = isset($detail["addonValue"]) ? trim($detail["addonValue"]) : "";
            $mItemID    = isset($detail["mItemID"])    ? (int) $detail["mItemID"]    : 0;
            $addonCode  = isset($detail["addonCode"]) && $detail["addonCode"] != "" ? trim($detail["addonCode"]) : "PIINSTALMENT";

            if ($addonValue != "" || $mItemID > 0) {
                $sqlAddon = "INSERT INTO jurnal_addon (
                        jurnalAddOnJurnalID,
                        jurnalAddOnJurnalTxID,
                        jurnalAddOnCode,
                        jurnalAddOnValue,
                        jurnalAddOnM_ItemID,
                        jurnalAddOnCreated,
                        jurnalAddOnCreatedUserID
                    ) VALUES (?,?,?,?,?,NOW(),?)";

                $qryAddon = $this->db->query($sqlAddon, [
                    $jurnalID,
                    $jurnalTxID,
                    $addonCode,
                    $addonValue,
                    $mItemID > 0 ? $mItemID : null,
                    $userID
                ]);
                if (!$qryAddon) {
                    $this->db->trans_rollback();
                    $this->sys_error_db("Gagal insert jurnal_addon pada detail ke-" . ($idx + 1));
                    return false;
                }
                $createdAddonCount++;
            }
        }

        if ($this->db->trans_status() === false) {
            $this->db->trans_rollback();
            $this->sys_error_db("Transaksi jurnal gagal.");
            return false;
        }

        $this->db->trans_commit();

        return [
            "jurnalID"    => $jurnalID,
            "jurnalNo"    => $jurnalNo,
            "detailCount" => count($details),
            "addonCount"  => $createdAddonCount
        ];
    }

    /**
     * generateJournalNumber — Generate nomor jurnal otomatis.
     *
     * Memanggil stored function MySQL fn_numbering('J') untuk mendapatkan
     * nomor jurnal berikutnya sesuai urutan penomoran yang berlaku.
     *
     * @return string|false  Nomor jurnal (misal: "J/2025/07/0001"), atau false jika gagal.
     */
    private function generateJournalNumber()
    {
        $qry = $this->db->query("SELECT fn_numbering('J') AS JNumber");
        if (!$qry || $qry->num_rows() == 0) {
            return false;
        }
        return $qry->row_array()["JNumber"];
    }

    /**
     * getPeriodeIDByDate — Ambil ID periode akuntansi berdasarkan tanggal transaksi.
     *
     * Periode diambil dari tabel `periode` yang aktif dan mencakup tanggal yang diberikan.
     * Digunakan untuk mengisi field jurnalperiodeID pada header jurnal.
     *
     * @param string $date  Tanggal transaksi (format YYYY-MM-DD)
     * @return int          periodeID yang sesuai, atau 0 jika tidak ditemukan
     */
    private function getPeriodeIDByDate($date)
    {
        $sql = "SELECT periodeID
                FROM periode
                WHERE periodeIsActive = 'Y'
                    AND DATE(?) BETWEEN periodeStartDate AND periodeEndDate
                LIMIT 1";
        $qry = $this->db->query($sql, [$date]);
        if (!$qry || $qry->num_rows() == 0) {
            return 0;
        }
        return (int) $qry->row_array()["periodeID"];
    }

    /**
     * getBranchCompanyIDByBranchCode — Ambil ID branch company berdasarkan kode cabang.
     *
     * Data diambil dari tabel `m_branch_companydetail` yang aktif.
     * Digunakan untuk mengisi field jurnalM_BranchCompanyID pada header jurnal.
     *
     * @param string $branchCode  Kode cabang (misal: "JKT-01")
     * @return int                branchCompanyID yang sesuai, atau 0 jika tidak ditemukan
     */
    private function getBranchCompanyIDByBranchCode($branchCode)
    {
        $sql = "SELECT M_BranchCompanyDetailM_BranchCompanyID AS branchCompanyID
                FROM m_branch_companydetail
                WHERE M_BranchCompanyDetailM_BranchCode = ?
                    AND M_BranchCompanyDetailIsActive = 'Y'
                LIMIT 1";
        $qry = $this->db->query($sql, [$branchCode]);
        if (!$qry || $qry->num_rows() == 0) {
            return 0;
        }
        return (int) $qry->row_array()["branchCompanyID"];
    }

    /**
     * getRegionalIDByBranchCode — Ambil ID regional berdasarkan kode cabang.
     *
     * Data diambil dari tabel `m_branch` yang aktif.
     * Digunakan untuk mengisi field JurnalS_RegionalID pada header jurnal.
     *
     * @param string $branchCode  Kode cabang (misal: "JKT-01")
     * @return int                regionalID yang sesuai, atau 0 jika tidak ditemukan
     */
    private function getRegionalIDByBranchCode($branchCode)
    {
        $sql = "SELECT M_BranchS_RegionalID AS regionalID
                FROM m_branch
                WHERE M_BranchCode = ?
                    AND M_BranchIsActive = 'Y'
                LIMIT 1";
        $qry = $this->db->query($sql, [$branchCode]);
        if (!$qry || $qry->num_rows() == 0) {
            return 0;
        }
        return (int) $qry->row_array()["regionalID"];
    }

    /**
     * getJurnalTypeID — Ambil ID tipe jurnal berdasarkan kode tipe.
     *
     * Data diambil dari tabel `jurnal_type` yang aktif.
     * Untuk PI cicilan order aset, kode yang digunakan adalah "PAYMENT".
     *
     * @param string $jurnalTypeCode  Kode tipe jurnal (misal: "PAYMENT")
     * @return int                    JurnalTypeID yang sesuai, atau 0 jika tidak ditemukan
     */
    private function getJurnalTypeID($jurnalTypeCode)
    {
        $sql = "SELECT JurnalTypeID
                FROM jurnal_type
                WHERE JurnalTypeCode = ?
                    AND JurnalTypeIsActive = 'Y'
                LIMIT 1";
        $qry = $this->db->query($sql, [$jurnalTypeCode]);
        if (!$qry || $qry->num_rows() == 0) {
            return 0;
        }
        return (int) $qry->row_array()["JurnalTypeID"];
    }

    /**
     * getEligiblePurchaseOrderAssetContracts — Ambil kontrak aset yang layak dibuatkan jurnal.
     *
     * Query ini mengambil semua kontrak dari `purchase_order_asset_contract` yang memenuhi
     * syarat berikut:
     *   - Kontrak aktif, status "belum lunas", nilai cicilan > 0
     *   - Cicilan terbayar masih kurang dari total cicilan
     *   - Tanggal kontrak masuk dalam rentang periode yang diberikan
     *   - Tanggal jatuh tempo cicilan sudah lewat atau sama dengan hari ini
     *   - PO sudah Approved dan aktif
     *   - Barang sudah diterima dan receive order sudah confirmed
     *
     * Data yang dikembalikan mencakup:
     *   - Detail kontrak (ID, nama, tanggal, nilai cicilan)
     *   - Data PO (nomor PO, supplier, payment term, warehouse)
     *   - Data cabang (kode cabang, nama, regionalID, branchCompanyID)
     *   - Data item PO pertama (sebagai referensi baris jurnal)
     *
     * @param string $startDate  Awal periode filter (YYYY-MM-DD)
     * @param string $endDate    Akhir periode filter (YYYY-MM-DD)
     * @return array             Array of row kontrak yang eligible
     */
    private function getEligiblePurchaseOrderAssetContracts($startDate, $endDate)
    {
        $sql = "SELECT
                    c.PurchaseOrderAssetContractID,
                    si.SupplierInvoiceID,
                    si.SupplierInvoiceNumber,
                    c.PurchaseOrderAssetContractPurchaseOrderID,
                    c.PurchaseOrderAssetContractName,
                    c.PurchaseOrderAssetContractStartDate,
                    c.PurchaseOrderAssetContractEndDate,
                    c.PurchaseOrderAssetContractInstallmentNumber,
                    c.PurchaseOrderAssetContractInstallmentPaid,
                    c.PurchaseOrderAssetContractInstallmentDate,
                    c.PurchaseOrderAssetContractInstallmentPayAmount,
                    c.PurchaseOrderAssetContractReceiveOrderPoID,
                    po.PurchaseOrderID,
                    po.PurchaseOrderNumber,
                    po.PurchaseOrderSupplierID,
                    po.PurchaseOrderPaymentTerm,
                    po.PurchaseOrderWarehouseType,
                    po.PurchaseOrderWarehouseID,
                    wh.WarehouseCode,
                    wh.WarehouseName,
                    wh.WarehouseType,
                    wh.WarehouseS_RegionalID AS WarehouseS_RegionalID,
                    wh.WarehouseM_BranchID AS WarehouseM_BranchID,
                    ro.ReceiveOrderPoID,
                    ro.ReceiveOrderPoConfirmed,
                    mi.M_ItemID AS ItemID,
                    mi.M_ItemCode AS ItemCode,
                    mi.M_ItemDesc AS ItemDesc,
                    mi.M_ItemFa_ClassID,
                    fc.Fa_ClassID AS ItemFaClassID,
                    fc.Fa_ClassName AS ItemFaClassName,
                    coa_bayar.coaID AS ItemCoaID,
                    coa_bayar.coaAccountNo AS ItemCoaAccountNo,
                    coa_bayar.coaDescription AS ItemCoaDesc,
                    coa_hutang.coaID AS HutangCoaID,
                    coa_hutang.coaAccountNo AS HutangCoaAccountNo,
                    coa_hutang.coaDescription AS HutangCoaDesc,
                    m_branch.M_BranchCode,
                    m_branch.M_BranchName,
                    m_branch.M_BranchS_RegionalID,
                    m_branch_companydetail.M_BranchCompanyDetailM_BranchCompanyID AS M_BranchCompanyID,
                    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'

                -- Data warehouse/gudang tujuan PO
                LEFT JOIN warehouse wh
                    ON wh.WarehouseID = po.PurchaseOrderWarehouseID

                -- Ambil 1 item PO pertama sebagai referensi baris jurnal
                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

                -- Hubungkan dengan detail supplier_invoice_detail terlebih dahulu
                JOIN supplier_invoice_detail sid
                    ON sid.SupplierInvoiceDetailPurchaseOrderID = po.PurchaseOrderID
                    AND sid.SupplierInvoiceDetailPurchaseOrderSummaryID = ps.PurchaseOrderSummaryID
                    AND sid.SupplierInvoiceDetailIsActive = 'Y'

                -- Baru hubungkan ke header supplier_invoice untuk mengambil nomor invoice
                JOIN supplier_invoice si
                    ON si.SupplierInvoiceID = sid.SupplierInvoiceDetailSupplierInvoiceID
                    AND si.SupplierInvoiceDate >= DATE(?)
                    AND si.SupplierInvoiceDate <= DATE(?)
                    AND si.SupplierInvoiceIsActive = 'Y'

                -- Pastikan barang sudah diterima (receive order confirmed)
                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

                -- Data cabang dari warehouse
                LEFT JOIN m_branch
                    ON m_branch.M_BranchID = wh.WarehouseM_BranchID
                    AND m_branch.M_BranchIsActive = 'Y'

                -- Data branch company untuk header jurnal
                LEFT JOIN m_branch_companydetail
                    ON m_branch_companydetail.M_BranchCompanyDetailM_BranchCode = m_branch.M_BranchCode
                    AND m_branch_companydetail.M_BranchCompanyDetailIsActive = 'Y'

                -- COA jurnal diambil dari item -> fa_class
                LEFT JOIN m_item mi
                    ON mi.M_ItemID = ps.PurchaseOrderSummaryItemID
                    AND mi.M_ItemIsActive = 'Y'
                LEFT JOIN fa_class fc
                    ON fc.Fa_ClassID = mi.M_ItemFa_ClassID
                    AND fc.Fa_ClassIsActive = 'Y'

                -- Join langsung ke tabel coa untuk memastikan kevalidan akun dan deskripsi
                LEFT JOIN coa coa_hutang
                    ON coa_hutang.coaID = fc.Fa_ClassHutangCoaID
                    AND coa_hutang.coaIsActive = 'Y'
                LEFT JOIN coa coa_bayar
                    ON coa_bayar.coaID = fc.Fa_ClassCoaID
                    AND coa_bayar.coaIsActive = 'Y'

                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 periode
                    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 tiba
                    AND IFNULL(c.PurchaseOrderAssetContractInstallmentDate, 1) <= ?
                    -- Pastikan belum pernah dibuatkan jurnal untuk invoice cicilan ini
                    AND NOT EXISTS (
                        SELECT 1
                        FROM jurnal_addon ja
                        JOIN jurnal j ON j.jurnalID = ja.jurnalAddOnJurnalID
                        WHERE ja.jurnalAddOnCode = 'PIINSTALMENT'
                            AND ja.jurnalAddOnValue = si.SupplierInvoiceNumber
                    )
                ORDER BY c.PurchaseOrderAssetContractID ASC";

        $qry = $this->db->query($sql, [
            $startDate,
            $endDate,
            $endDate,
            $startDate,
            (int) date("d", strtotime($endDate))
        ]);
        if (!$qry) {
            $this->sys_error_db("Gagal mengambil data purchase_order_asset_contract.");
            exit;
        }

        return $qry->result_array();
    }

    /**
     * isValidDate — Validasi format tanggal YYYY-MM-DD.
     *
     * @param string $tanggal  String tanggal yang akan divalidasi
     * @return bool            true jika format valid, false jika tidak
     */
    private function isValidDate($tanggal)
    {
        $d = DateTime::createFromFormat("Y-m-d", $tanggal);
        return $d && $d->format("Y-m-d") === $tanggal;
    }
}
