Buat Paste Baru
Paste Terbaru
Untitled (e3648e4c)
2026-06-11 12:12:20

reg query "HKLM\SOFTWARE\Policies\Microsoft\Windows\RemovableStorageDevices" /s
reg query "HKCU\SOFTWARE\Policies\Micro ...

Untitled (e46015bd)
2026-06-11 11:57:43

--NIPDIPROSES 810450539,954500220 --proxy http://10.12.11.69:8080 --multiplier 4

Untitled (699d3391)
2026-06-09 08:54:22

120363425343637418@g

Untitled (62bc9e35)
2026-06-03 10:36:57

instruksi awal: nanti jika untuk element pada web saya ngasihnya berupa xpath atau hal lain, jika memungkinkan pakai saj ...

Untitled (d24a4a4f)
2026-06-01 21:49:46

php -r "echo password_hash('PasswordAndaYangKuat', PASSWORD_DEFAULT), PHP_EOL;"

Untitled (30aff1a1)
2026-05-25 01:40:29

#!/usr/bin/env python3
"""Coretax Self-Service Billing automation.

Workflow:
1. Fetch list of billing requests ...

Untitled (59de8631)
2026-05-13 02:50:58

https://t.kemenkeu.go.id/SosialisasiNewSIKKA

Untitled (25e5ba71)
2026-05-11 04:25:47

=== Memproses data ke-45 (row 46) ===
SUB_CASE : C0020476335
SUB_CASE = C0020476335
NOMOR_ST = ST ...

Untitled (ff4ac3c6)
2026-05-11 03:49:47

=== Memproses data ke-8 (row 9) ===
SUB_CASE : C0020471420
SUB_CASE = C0020471420
NOMOR_ST = ST-0 ...

Untitled (f3648f92)
2026-05-08 13:23:34

120363427904873165

Untitled (6d3bcf66)
2026-05-08 12:33:05

✨ *KINERJA PENERIMAAN*
🏢 *KPP Pratama Banyuwangi*
🗓 Update Data: 05 Mei 2026 07:30:15 WIB (Sumber DRM)

📢 *Rencana P ...

Untitled (b8ed0ab8)
2026-04-28 07:51:07

PS C:\Users\DAC\Documents\spt.batu.kemenkeu.one> & C:/Users/DAC/Documents/spt.batu.kemenkeu.one/venv/Scripts/python.exe ...

Untitled (1a215875)
2026-04-27 04:22:09

@echo off
echo [%date% %time%] Stopping iphlpsvc...
net stop iphlpsvc

echo [%date% %time%] Waiting 5 seconds...
ti ...

Untitled (cef1254c)
2026-04-22 06:28:03

pengaruh factor internl dan factor eksternal terhadap kepatuhan wajib pajak dalam melaporkan dan membayar pajak
factor ...

Untitled (313b3eeb)
2026-04-22 05:18:25

Rapat Microsoft Teams
Bergabung: https://teams.microsoft.com/meet/44660973024892?p=V9dSb9qgt7XGAxuqFu
ID Rapat: 446 60 ...

Untitled (581b87bd)
2026-04-21 14:11:58

1. buka url https://coretax.intranet.pajak.go.id/ tunggu sampai page load selesai dibuka dengan sempurna
2. pada xpath ...

Untitled (b37f5a6a)
2026-04-15 09:05:17

https://kemenkeu-my.sharepoint.com/:x:/g/personal/dymas_sedhayu_kemenkeu_go_id/IQDxKb87K95tSq0J5byxmYDnAQvQw4XUSDvRprJEc ...

Untitled (89796166)
2026-04-06 04:59:49

http://localhost:1455/auth/callback?code=ac_zXsNPU8mUkW2RHk14A29c1spfwrfG_UiFa7wQLedXCs.ggOP19-NDnX_1jUw4PZXX5B6zmHghDRt ...

Untitled (9d5938d5)
2026-03-27 11:52:22

POST /WhatsApp/xml-data-post.php
Authorization: Bearer bwi627
Content-Type: multipart/form-data

ID=...&STATUS=sukse ...

Untitled (4c72de08)
2026-03-26 11:23:24

https://portal.kppbwi.id/WhatsApp/xml-data-req.php?token=a3f8c2e1d74b905f6e3a1c8d2b047e9f

Untitled

ID: 30aff1a1 • 2026-05-25 01:40:29

#!/usr/bin/env python3
"""Coretax Self-Service Billing automation.

Workflow:
    1. Fetch list of billing requests from portal XML feed.
    2. Ensure Coretax SSO session (reuse persistent profile or fresh login).
    3. Loop each XML item, fill the self-service billing form, submit and
       download the resulting BILLING_CODE_*.pdf into Coretax/billing/.
    4. Extract Kode Billing from the PDF.
    5. POST status (and PDF when available) back to portal.

Usage:
    python coretax-billing.py
"""

import json
import logging
import os
import re
import time
import xml.etree.ElementTree as ET
from pathlib import Path

import requests
from selenium import webdriver
from selenium.common.exceptions import TimeoutException, WebDriverException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

try:
    from PyPDF2 import PdfReader
except Exception:
    PdfReader = None

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
log = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Paths & constants
# ---------------------------------------------------------------------------
BASE = Path('/Users/dymas/.openclaw/workspace/Project/Python')
DRIVER_PATH = BASE / 'driver' / 'chromedriver'
CHROME_APP_PATH = Path('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome')
PROFILE_DIR = BASE / 'session' / 'coretax-intranet-profile'
COOKIE_DEBUG_FILE = BASE / 'session' / 'coretax-intranet.json'
BILLING_DIR = BASE / 'Coretax' / 'billing'

TARGET_URL = 'https://coretax.intranet.pajak.go.id'
BILLING_URL = 'https://coretax.intranet.pajak.go.id/payment/id-ID/self-billing'
PROXY = 'http://10.12.11.69:8080'

USERNAME = 'dymas.sedhayu'
PASSWORD = 'K3lu@rg4Cemenkau'

CORETAX_HOST = 'coretax.intranet.pajak.go.id'
SSO_HOST = 'satu.intranet.pajak.go.id'

XML_REQ_URL = 'https://portal.kppbwi.id/WhatsApp/xml-data-req.php?token=a3f8c2e1d74b905f6e3a1c8d2b047e9f'
XML_POST_URL = 'https://portal.kppbwi.id/WhatsApp/xml-data-post.php?token=a3f8c2e1d74b905f6e3a1c8d2b047e9f'

KODE_MAP_NEED_NOP = {'411128-402', '411128-432', '411211-103'}

LOGIN_USERNAME_SELECTORS = [
    (By.NAME, 'username'),
    (By.XPATH, "//input[@placeholder='Email ID']"),
]
LOGIN_PASSWORD_SELECTORS = [
    (By.NAME, 'password'),
    (By.ID, 'passwordInput'),
    (By.XPATH, "//input[@placeholder='Password']"),
]
LOGIN_SUBMIT_SELECTORS = [
    (By.ID, 'kt_sign_in_submit'),
    (By.XPATH, "//button[@type='submit' and contains(normalize-space(.), 'Masuk')]"),
]


# ---------------------------------------------------------------------------
# URL state helpers
# ---------------------------------------------------------------------------

def classify_url(url: str) -> str:
    u = url.lower()
    if CORETAX_HOST in u and 'auth-callback' not in u:
        return 'coretax-home'
    if SSO_HOST in u and 'auth-callback' in u:
        return 'auth-callback'
    if SSO_HOST in u and re.search(r'/(login|signin)', u):
        return 'sso-login'
    if SSO_HOST in u:
        return 'sso-other'
    if CORETAX_HOST in u:
        return 'coretax-callback'
    return 'unknown'


def visible_first(driver, selectors):
    for by, selector in selectors:
        for element in driver.find_elements(by, selector):
            if element.is_displayed() and element.is_enabled():
                return element
    return None


def click_first(driver, selectors, label: str):
    element = visible_first(driver, selectors)
    if element is None:
        raise RuntimeError(f'Elemen untuk {label} tidak ditemukan')
    driver.execute_script('arguments[0].scrollIntoView({block:"center"});', element)
    time.sleep(0.5)
    driver.execute_script('arguments[0].click();', element)
    log.info('Klik %s', label)
    return element


def is_sso_login_page(driver) -> bool:
    state = classify_url(driver.current_url)
    if state == 'sso-login':
        return True
    if state in ('coretax-home', 'auth-callback', 'coretax-callback'):
        return False
    has_user = visible_first(driver, LOGIN_USERNAME_SELECTORS) is not None
    has_pass = visible_first(driver, LOGIN_PASSWORD_SELECTORS) is not None
    return has_user and has_pass


def is_coretax_home(driver) -> bool:
    return classify_url(driver.current_url) == 'coretax-home'


def wait_for_page_load(driver, timeout: int = 30):
    WebDriverWait(driver, timeout).until(
        lambda d: d.execute_script('return document.readyState') == 'complete'
    )


# ---------------------------------------------------------------------------
# Driver
# ---------------------------------------------------------------------------

def build_driver() -> webdriver.Chrome:
    if not DRIVER_PATH.exists():
        raise FileNotFoundError(f'Chromedriver tidak ditemukan di: {DRIVER_PATH}')
    if not CHROME_APP_PATH.exists():
        raise FileNotFoundError(f'Google Chrome tidak ditemukan di: {CHROME_APP_PATH}')
    PROFILE_DIR.mkdir(parents=True, exist_ok=True)
    BILLING_DIR.mkdir(parents=True, exist_ok=True)
    log.info('[DRIVER] Profile dir: %s', PROFILE_DIR)
    log.info('[DRIVER] Download dir: %s', BILLING_DIR)

    options = Options()
    options.binary_location = str(CHROME_APP_PATH)
    options.add_argument(f'--user-data-dir={PROFILE_DIR}')
    options.add_argument('--start-maximized')
    options.add_argument('--no-default-browser-check')
    options.add_argument('--disable-notifications')
    options.add_argument('--disable-popup-blocking')
    options.add_argument('--ignore-certificate-errors')
    options.add_argument('--allow-insecure-localhost')
    options.add_argument(f'--proxy-server={PROXY}')

    prefs = {
        'download.default_directory': str(BILLING_DIR),
        'download.prompt_for_download': False,
        'download.directory_upgrade': True,
        'plugins.always_open_pdf_externally': True,
        'profile.default_content_settings.popups': 0,
        'safebrowsing.enabled': True,
        'safebrowsing.disable_download_protection': True,
    }
    options.add_experimental_option('prefs', prefs)

    return webdriver.Chrome(service=Service(str(DRIVER_PATH)), options=options)


def _save_cookies_debug(driver):
    try:
        COOKIE_DEBUG_FILE.parent.mkdir(parents=True, exist_ok=True)
        cookies = driver.get_cookies()
        with open(COOKIE_DEBUG_FILE, 'w', encoding='utf-8') as fh:
            json.dump(cookies, fh, indent=2, ensure_ascii=False)
    except Exception as exc:
        log.debug('[DEBUG] Cookie export gagal: %s', exc)


# ---------------------------------------------------------------------------
# Auth flow (mirrors coretax-aktivasi-kose.py)
# ---------------------------------------------------------------------------

def wait_for_authenticated_landing(driver, timeout: int = 90) -> bool:
    end_time = time.time() + timeout
    last_url = None
    seen_callback = False
    while time.time() < end_time:
        try:
            current = driver.current_url
        except WebDriverException:
            time.sleep(1)
            continue
        state = classify_url(current)
        if current != last_url:
            log.info('[TRANSITION] %-16s %s', state.upper(), current)
            last_url = current
        if state == 'auth-callback':
            seen_callback = True
        if state == 'coretax-home':
            try:
                wait_for_page_load(driver, 15)
            except TimeoutException:
                pass
            log.info('[HOME] Authenticated landing dikonfirmasi: %s', current)
            return True
        if state == 'sso-login' and seen_callback:
            log.warning('[LOGIN-FAILED] Redirect kembali ke SSO login: %s', current)
            return False
        time.sleep(1)
        try:
            wait_for_page_load(driver, 8)
        except TimeoutException:
            pass
    log.warning('[TIMEOUT] Tidak mencapai Coretax home dalam %ds. URL: %s', timeout, last_url)
    return False


def check_profile_session(driver) -> bool:
    log.info('[PROFILE] Memeriksa sesi dari persistent profile...')
    try:
        driver.get(TARGET_URL)
        wait_for_page_load(driver, 30)
    except TimeoutException:
        log.warning('[PROFILE] Timeout navigasi ke Coretax home')
    time.sleep(3)
    if is_coretax_home(driver):
        log.info('[PROFILE] Sesi profile valid')
        return True
    if is_sso_login_page(driver):
        log.info('[PROFILE] Sesi profile tidak valid - perlu fresh login')
        return False
    return wait_for_authenticated_landing(driver, timeout=30)


def do_login(driver) -> bool:
    log.info('[LOGIN] Memulai login flow')
    try:
        driver.get(TARGET_URL)
        wait_for_page_load(driver, 30)
    except TimeoutException:
        pass
    time.sleep(2)
    if is_coretax_home(driver):
        return True
    if not is_sso_login_page(driver):
        deadline = time.time() + 20
        while time.time() < deadline:
            time.sleep(2)
            if is_sso_login_page(driver):
                break
            if is_coretax_home(driver):
                return True
    if not is_sso_login_page(driver):
        log.error('[LOGIN] Halaman SSO tidak ditemukan: %s', driver.current_url)
        return False

    username_el = visible_first(driver, LOGIN_USERNAME_SELECTORS)
    if username_el is None:
        log.error('[LOGIN] Field username tidak ditemukan')
        return False
    username_el.clear()
    username_el.send_keys(USERNAME)

    password_el = visible_first(driver, LOGIN_PASSWORD_SELECTORS)
    if password_el is None:
        log.error('[LOGIN] Field password tidak ditemukan')
        return False
    password_el.clear()
    password_el.send_keys(PASSWORD)

    click_first(driver, LOGIN_SUBMIT_SELECTORS, 'tombol Masuk')
    return wait_for_authenticated_landing(driver, timeout=90)


def ensure_logged_in(driver):
    if check_profile_session(driver):
        log.info('[AUTH] Sesi profile valid')
        _save_cookies_debug(driver)
        return
    if not do_login(driver):
        raise RuntimeError('Login gagal')
    _save_cookies_debug(driver)
    log.info('[AUTH] Fresh login berhasil')


# ---------------------------------------------------------------------------
# XML helpers
# ---------------------------------------------------------------------------

def fetch_xml_items():
    log.info('[XML] Fetching %s', XML_REQ_URL)
    r = requests.get(XML_REQ_URL, timeout=30)
    r.raise_for_status()
    text = r.text.strip()
    if not text:
        return []
    try:
        root = ET.fromstring(text)
    except ET.ParseError as e:
        log.error('[XML] Gagal parse XML: %s', e)
        return []
    items = []
    for it in root.findall('.//item'):
        rec = {}
        for child in it:
            rec[child.tag] = (child.text or '').strip()
        items.append(rec)
    return items


# ---------------------------------------------------------------------------
# PDF / download helpers
# ---------------------------------------------------------------------------

def wait_for_billing_pdf(dirpath: Path, existing: set, timeout: int = 60):
    start = time.time()
    while time.time() - start < timeout:
        try:
            files = [f for f in os.listdir(dirpath)
                     if f.lower().endswith('.pdf') or f.lower().endswith('.crdownload')]
        except Exception:
            files = []
        new = [f for f in files if f not in existing]
        completed = [f for f in new
                     if f.lower().endswith('.pdf') and not f.lower().endswith('.crdownload')]
        billing_match = [f for f in completed if f.upper().startswith('BILLING_CODE_')]
        target = billing_match or completed
        if target:
            target.sort(key=lambda p: os.path.getmtime(dirpath / p), reverse=True)
            return dirpath / target[0]
        time.sleep(0.5)
    return None


def extract_kode_billing(path: Path) -> str:
    if PdfReader is None:
        log.warning('PyPDF2 tidak tersedia')
        return ''
    try:
        reader = PdfReader(str(path))
        full = '\n'.join((p.extract_text() or '') for p in reader.pages)
    except Exception as e:
        log.warning('Gagal baca PDF: %s', e)
        return ''
    # Kode Billing biasanya 15 digit
    m = re.search(r'Kode\s*Billing[^\d]{0,40}(\d[\d\s]{13,20}\d)', full, re.IGNORECASE)
    if m:
        return re.sub(r'\D', '', m.group(1))
    m = re.search(r'\b\d{15}\b', full)
    if m:
        return m.group(0)
    return ''


# ---------------------------------------------------------------------------
# Billing flow
# ---------------------------------------------------------------------------

def _wait_clickable(driver, xpath, timeout):
    return WebDriverWait(driver, timeout).until(EC.element_to_be_clickable((By.XPATH, xpath)))


def _wait_present(driver, xpath, timeout):
    return WebDriverWait(driver, timeout).until(EC.presence_of_element_located((By.XPATH, xpath)))


def _fill_dropdown_global(driver, dropdown_xpath, value, item_xpath_options=None, timeout=10):
    """Click a p-dropdown that opens an overlay attached to body, type value, pick first."""
    _wait_clickable(driver, dropdown_xpath, timeout).click()
    time.sleep(0.5)
    inp_xpath = '/html/body/div/div/div/div[1]/div/input'
    inp = _wait_present(driver, inp_xpath, timeout)
    WebDriverWait(driver, timeout).until(lambda d: inp.is_enabled())
    time.sleep(1)
    inp.clear()
    inp.send_keys(value)
    time.sleep(1)
    candidates = item_xpath_options or [
        '/html/body/div/div/div/div[2]/ul/p-dropdownitem/li/span',
        '/html/body/div/div/div/div[2]/p-scroller/div/ul/p-dropdownitem/li/span',
    ]
    for ix in candidates:
        try:
            _wait_clickable(driver, ix, timeout).click()
            return
        except TimeoutException:
            continue
    raise TimeoutException(f'Dropdown item tidak muncul untuk value: {value}')


def process_billing(driver, item: dict) -> dict:
    """Run the billing creation for a single XML item.

    Returns dict with keys: status, kode_billing, pdf_path
    """
    npwp16 = item.get('NPWP16') or ''
    nik = item.get('NIK') or ''
    kode_map = item.get('KODE_MAP_BAYAR') or ''
    masa = item.get('MASA_TAHUN_PAJAK') or ''
    nop = item.get('NOP') or ''
    alamat_nop = item.get('ALAMAT_NOP') or ''
    prov_nop = item.get('PROV_NOP') or ''
    kab_nop = item.get('KAB_NOP') or ''
    kec_nop = item.get('KEC_NOP') or ''
    kel_nop = item.get('KEL_NOP') or ''
    jumlah = item.get('JUMLAH') or ''
    uraian = item.get('URAIAN') or ''

    search_value = npwp16 or nik
    log.info('[BILLING] search=%s kode_map=%s masa=%s jumlah=%s',
             search_value, kode_map, masa, jumlah)

    result = {'status': 'gagal', 'kode_billing': '', 'pdf_path': None}

    driver.get(BILLING_URL)
    wait_for_page_load(driver, 60)
    time.sleep(2)

    # Step 1: Input search NPWP/NIK
    search_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                    'pmnt-self-service-taxpayer-search-grid/pmnt-taxpayer-search-grid/'
                    'ui-taxpayer-search-grid-2/div[1]/div/input')
    inp = _wait_present(driver, search_xpath, 60)
    inp.clear()
    inp.send_keys(search_value)

    # Step 2: Wait for result row & click select button
    select_btn_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                        'pmnt-self-service-taxpayer-search-grid/pmnt-taxpayer-search-grid/'
                        'ui-taxpayer-search-grid-2/p-table/div/div/table/tbody/tr/td[1]/div/div/p-button/button')
    _wait_present(driver, select_btn_xpath, 60)
    btn = _wait_clickable(driver, select_btn_xpath, 60)
    time.sleep(1)
    btn.click()

    # Step 3: Click "Tambah Detail Pembayaran" button
    add_detail_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                        'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                        'form/div/div/div/div[4]/div/div[2]/div[4]/button')
    _wait_clickable(driver, add_detail_xpath, 30).click()

    # Step 4: KODE_MAP_BAYAR dropdown
    kode_dropdown_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                           'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                           'form/div/div/div/div[4]/div/div[2]/div[3]/div[2]/p-dropdown')
    _wait_clickable(driver, kode_dropdown_xpath, 30).click()
    kode_input_xpath = kode_dropdown_xpath + '/div/p-overlay/div/div/div/div[1]/div/input'
    kode_input = _wait_present(driver, kode_input_xpath, 10)
    time.sleep(1)
    kode_input.clear()
    kode_input.send_keys(kode_map)
    time.sleep(1)
    kode_item_xpath = kode_dropdown_xpath + '/div/p-overlay/div/div/div/div[2]/ul/p-dropdownitem/li/span'
    _wait_clickable(driver, kode_item_xpath, 10).click()
    time.sleep(2)

    # Step 5: MASA_TAHUN_PAJAK dropdown
    masa_dropdown_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                           'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                           'form/div/div/div/div[4]/div/div[2]/div[4]/div[2]/p-dropdown')
    _wait_clickable(driver, masa_dropdown_xpath, 10).click()
    time.sleep(1)
    masa_input_xpath = masa_dropdown_xpath + '/div/p-overlay/div/div/div/div[1]/div/input'
    masa_input = _wait_present(driver, masa_input_xpath, 10)
    time.sleep(1)
    masa_input.clear()
    masa_input.send_keys(masa)
    time.sleep(1)
    masa_item_xpath = masa_dropdown_xpath + '/div/p-overlay/div/div/div/div[2]/ul/p-dropdownitem/li/span'
    _wait_clickable(driver, masa_item_xpath, 10).click()
    time.sleep(1)

    # Step 6: NOP block (only when KODE_MAP_BAYAR matches or radio appears)
    radio_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                   'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                   'form/div/div/div/div[4]/div/div[2]/div[5]/div/div/div[1]/div[1]/p-radiobutton/div/div[2]')
    needs_nop = kode_map in KODE_MAP_NEED_NOP
    if not needs_nop:
        try:
            WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.XPATH, radio_xpath)))
            needs_nop = True
        except TimeoutException:
            needs_nop = False

    if needs_nop:
        log.info('[BILLING] Mengisi blok NOP')
        _wait_clickable(driver, radio_xpath, 10).click()

        nop_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                     'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                     'form/div/div/div/div[4]/div/div[2]/div[5]/div/div/div[4]/div[2]/input')
        nop_inp = _wait_clickable(driver, nop_xpath, 10)
        nop_inp.clear()
        nop_inp.send_keys(nop)

        alamat_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                        'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                        'form/div/div/div/div[4]/div/div[2]/div[5]/div/div/div[5]/div[2]/input')
        alamat_inp = _wait_clickable(driver, alamat_xpath, 10)
        alamat_inp.clear()
        alamat_inp.send_keys(alamat_nop)

        prov_dd = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                   'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                   'form/div/div/div/div[4]/div/div[2]/div[5]/div/div/div[6]/div[2]/ui-dropdown/p-dropdown')
        _fill_dropdown_global(driver, prov_dd, prov_nop)

        kab_dd = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                  'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                  'form/div/div/div/div[4]/div/div[2]/div[5]/div/div/div[7]/div[2]/ui-dropdown/p-dropdown')
        _fill_dropdown_global(driver, kab_dd, kab_nop)

        kec_dd = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                  'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                  'form/div/div/div/div[4]/div/div[2]/div[5]/div/div/div[8]/div[2]/ui-dropdown/p-dropdown')
        _fill_dropdown_global(driver, kec_dd, kec_nop)

        kel_dd = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                  'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                  'form/div/div/div/div[4]/div/div[2]/div[5]/div/div/div[9]/div[2]/ui-dropdown/p-dropdown')
        _fill_dropdown_global(
            driver, kel_dd, kel_nop,
            item_xpath_options=[
                '/html/body/div/div/div/div[2]/p-scroller/div/ul/p-dropdownitem/li/span',
                '/html/body/div/div/div/div[2]/ul/p-dropdownitem/li/span',
            ],
        )

        save_nop_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                          'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                          'form/div/div/div/div[4]/div/div[2]/div[6]/div[2]/button')
        _wait_clickable(driver, save_nop_xpath, 10).click()
    else:
        save_alt_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                          'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                          'form/div/div/div/div[4]/div/div[2]/div[5]/div[2]/button')
        _wait_clickable(driver, save_alt_xpath, 30).click()

    time.sleep(1)

    # Step 7: JUMLAH
    jumlah_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                    'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                    'form/div/div/div/div[4]/div/div[2]/div[7]/div[2]/p-inputnumber/span/input')
    jumlah_inp = _wait_present(driver, jumlah_xpath, 30)
    jumlah_inp.click()
    jumlah_inp.send_keys(Keys.CONTROL, 'a')
    jumlah_inp.send_keys(Keys.DELETE)
    jumlah_inp.send_keys(jumlah)
    time.sleep(0.5)

    # Step 8: URAIAN (optional)
    if uraian:
        uraian_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                        'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                        'form/div/div/div/div[4]/div/div[2]/div[9]/div[2]/input')
        try:
            uraian_inp = _wait_present(driver, uraian_xpath, 30)
            uraian_inp.clear()
            uraian_inp.send_keys(uraian)
        except TimeoutException:
            log.warning('[BILLING] Field URAIAN tidak ditemukan')

    # Step 9: Submit "Untuk Kode Billing"
    existing = set(os.listdir(BILLING_DIR))
    submit_xpath = ('/html/body/pmnt-root/div/ui-coretax-one-column-layout/div/div/div/'
                    'pmnt-self-service-billing-code/pmntshr-self-service-billing-code-form/'
                    'form/div/div/div/div[4]/div/div[2]/div[10]/div[2]/button')
    _wait_clickable(driver, submit_xpath, 30).click()
    log.info('[BILLING] Submit kode billing')

    # Step 10: Wait for PDF download
    pdf_path = wait_for_billing_pdf(BILLING_DIR, existing, timeout=90)
    if not pdf_path:
        log.error('[BILLING] PDF tidak terdownload')
        return result
    log.info('[BILLING] PDF didownload: %s', pdf_path)
    result['pdf_path'] = str(pdf_path)

    # Step 11: Extract Kode Billing
    kode_billing = extract_kode_billing(pdf_path)
    result['kode_billing'] = kode_billing
    log.info('[BILLING] KODE_BILLING=%s', kode_billing)

    if pdf_path and kode_billing:
        result['status'] = 'sukses'

    return result


# ---------------------------------------------------------------------------
# Posting back
# ---------------------------------------------------------------------------

def post_result(item_id: str, status: str, kode_billing: str, pdf_path):
    data = {'ID': item_id, 'STATUS': status, 'IDBILLING': kode_billing}
    files = None
    fh = None
    try:
        if pdf_path:
            if os.path.exists(pdf_path):
                file_size = os.path.getsize(pdf_path)
                log.info('[POST] PDF ditemukan: %s (size=%d bytes)', pdf_path, file_size)
                if file_size == 0:
                    log.warning('[POST] File PDF kosong (0 bytes), tidak diupload')
                else:
                    fh = open(pdf_path, 'rb')
                    files = {'FILEBILLING': (os.path.basename(pdf_path), fh, 'application/pdf')}
            else:
                log.warning('[POST] pdf_path diset tapi file tidak ditemukan: %s', pdf_path)
        else:
            log.info('[POST] Tidak ada PDF untuk diupload')

        log.info('[POST] Kirim ke %s | data=%s | file=%s',
                 XML_POST_URL, data,
                 os.path.basename(pdf_path) if files else 'NONE')
        r = requests.post(XML_POST_URL, data=data, files=files, timeout=60)
        log.info('[POST] HTTP %s | response: %s', r.status_code, r.text)

        # Cek apakah server berhasil menyimpan file
        try:
            resp_json = r.json()
            saved_file = resp_json.get('file', None)
            if files and saved_file == '':
                log.warning('[POST] File dikirim tapi server tidak menyimpan (file=""). '
                            'Cek field name $_FILES pada server PHP.')
            elif files and saved_file:
                log.info('[POST] File berhasil disimpan di server: %s', saved_file)
        except Exception:
            pass  # Response bukan JSON, sudah terlog di atas

    except Exception as e:
        log.error('[POST] Gagal: %s', e)
    finally:
        if fh:
            fh.close()


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    log.info('=== Coretax Self-Billing Automation ===')

    items = fetch_xml_items()
    if not items:
        log.info('[XML] Tidak ada data, script dihentikan')
        return
    log.info('[XML] %d item ditemukan', len(items))

    driver = build_driver()
    try:
        ensure_logged_in(driver)
        log.info('=== Login dikonfirmasi - mulai loop billing ===')

        for idx, item in enumerate(items, start=1):
            item_id = item.get('ID', '')
            log.info('--- [%d/%d] ID=%s ---', idx, len(items), item_id)
            status = 'gagal'
            kode_billing = ''
            pdf_path = None
            try:
                res = process_billing(driver, item)
                status = res['status']
                kode_billing = res['kode_billing']
                pdf_path = res['pdf_path']
            except Exception as e:
                log.exception('[BILLING] Error pada ID=%s: %s', item_id, e)
            finally:
                post_result(item_id, status, kode_billing, pdf_path)
    finally:
        try:
            driver.quit()
        except Exception:
            pass
        log.info('Browser ditutup')


if __name__ == '__main__':
    main()