sys_input; $userID = !empty($this->sys_user["M_UserID"]) ? (int) $this->sys_user["M_UserID"] : 0; $startDate = !empty($para["startDate"]) ? $para["startDate"] : date("Y-m-01"); $endDate = !empty($para["endDate"]) ? $para["endDate"] : date("Y-m-t"); if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) { throw new Exception("Format tanggal tidak valid."); } if (strtotime($startDate) > strtotime($endDate)) { throw new Exception("startDate > endDate."); } // ── Steps 1–5: Get eligible contracts ──────────────────── $eligible = $this->getEligibleInstallments($startDate, $endDate); $created = []; $inserted = 0; $this->db->trans_begin(); foreach ($eligible as $row) { $amount = (float) $row["PurchaseOrderAssetContractInstallmentPayAmount"]; $dayOfMonth = (int) ($row["PurchaseOrderAssetContractInstallmentDate"] ?? 1); $lastDay = (int) date("t", strtotime($endDate)); $dayOfMonth = min($dayOfMonth, $lastDay); $installDate = date("Y-m", strtotime($endDate)) . "-" . str_pad($dayOfMonth, 2, "0", STR_PAD_LEFT); $dueDate = date("Y-m-d", strtotime($installDate . " +7 days")); $createdBy = (int) ($row["PurchaseOrderAssetContractCreatedUserID"] ?? $userID); // ── Step 6: INSERT supplier_installment ────────────── $sql = "INSERT INTO supplier_installment ( SupplierInstallmentPurchaseOrderID, SupplierInstallmentSupplierID, SupplierInstallmentSupplierInvoiceID, SupplierInstallmentAmount, SupplierInstallmentDate, SupplierInstallmentDueDate, SupplierInstallmentPaymentID, SupplierInstallmentStatus, SupplierInstallmentIsLunas, SupplierInstallmentIsActive, SupplierInstallmentCreated, SupplierInstallmentCreatedUserID ) VALUES (?, ?, ?, ?, ?, ?, 0, 'Pending', 'N', 'Y', NOW(), ?)"; $que = $this->db->query($sql, [ $row["PurchaseOrderID"], $row["PurchaseOrderSupplierID"], $row["SupplierInvoiceID"], $amount, $installDate, $dueDate, $createdBy ]); if (!$que) { $this->db->trans_rollback(); $this->sys_error_db("Gagal insert supplier_installment."); exit; } $installmentID = $this->db->insert_id(); $inserted++; $created[] = [ "installmentID" => $installmentID, "contractID" => $row["PurchaseOrderAssetContractID"], "purchaseOrderID" => $row["PurchaseOrderID"], "parentInvoiceID" => $row["SupplierInvoiceID"], "parentInvoiceNumber" => $row["SupplierInvoiceNumber"], "amount" => $amount, "installDate" => $installDate, "dueDate" => $dueDate ]; } if ($this->db->trans_status() === false) { $this->db->trans_rollback(); $this->sys_error_db("Transaksi gagal."); exit; } $this->db->trans_commit(); $this->sys_ok([ "startDate" => $startDate, "endDate" => $endDate, "totalEligible" => count($eligible), "totalInserted" => $inserted, "created" => $created ]); } catch (Exception $exc) { if ($this->db->trans_status() === false) { $this->db->trans_rollback(); } $this->sys_error($exc->getMessage()); } } /** * 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; } /** * getEligibleInstallments — Steps 1–5 combined * * Returns contracts that: * Step 1: Are active, not paid off, within date range, due date reached * Step 2: Have approved & active PO * Step 3: Have confirmed & active RO * Step 4: Have a parent supplier_invoice with IsInstallment = 'Y' * Step 5: Do NOT already have a supplier_installment for this month * * @param string $startDate YYYY-MM-DD * @param string $endDate YYYY-MM-DD * @return array */ private function getEligibleInstallments($startDate, $endDate) { $dayOfMonth = (int) date("d", strtotime($endDate)); $sql = "SELECT c.PurchaseOrderAssetContractID, c.PurchaseOrderAssetContractInstallmentPayAmount, c.PurchaseOrderAssetContractInstallmentDate, c.PurchaseOrderAssetContractCreatedUserID, po.PurchaseOrderID, po.PurchaseOrderSupplierID, si.SupplierInvoiceID, si.SupplierInvoiceNumber FROM purchase_order_asset_contract c -- Step 2: PO must be Approved and Active JOIN purchase_order po ON po.PurchaseOrderID = c.PurchaseOrderAssetContractPurchaseOrderID AND po.PurchaseOrderIsActive = 'Y' AND po.PurchaseOrderStatus = 'Approved' -- Step 3: RO must be Confirmed and Active 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 -- Step 4: Parent invoice must exist with IsInstallment = 'Y' JOIN supplier_invoice si ON si.SupplierInvoiceReceiveOrderPoID = ro.ReceiveOrderPoID AND si.SupplierInvoiceIsActive = 'Y' AND si.SupplierInvoiceIsInstallment = 'Y' -- Step 1: Contract eligibility 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) <= ? -- Step 5: Dedup — no existing supplier_installment this month (any status) AND NOT EXISTS ( SELECT 1 FROM supplier_installment inst WHERE inst.SupplierInstallmentIsActive = 'Y' AND inst.SupplierInstallmentPurchaseOrderID = po.PurchaseOrderID AND DATE_FORMAT(inst.SupplierInstallmentDate, '%Y-%m') = DATE_FORMAT(?, '%Y-%m') ) ORDER BY c.PurchaseOrderAssetContractID ASC"; $params = [$endDate, $startDate, $dayOfMonth, $endDate]; $qry = $this->db->query($sql, $params); if (!$qry) { $this->sys_error_db("Gagal mengambil daftar installment eligible."); exit; } return $qry->result_array(); } }