mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
feat(ownership): import IDX above-1% XLSX holder register
Since June 2026 IDX publishes the monthly "Pemegang Saham di Atas 1%" list as an XLSX workbook on the Data Kepemilikan Saham page instead of a PDF announcement. Add a parser for it and accept `.xlsx` in `ownership import --file`. - src/ownership/xlsx.rs: zip + quick-xml reader (shared strings incl. rich text, sheet cells), exact 12-column header match so the daily above-5% and investor-type/classification workbooks are rejected, Excel serial dates, numeric shares, percent -> bps with float-noise rounding. Builds KseiHoldingDraft directly and reuses the entity normalizers, so investor types stay uppercase like the PDF path. - Fixtures: trimmed excerpts of the real 2026-08-31 above-1% and 2026-09-23 above-5% workbooks. - Unit + CLI tests: import, ticker, releases, above-5% rejection. - Docs: ARCHITECTURE parser pipeline, AGENTS ownership inputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017a5Cv5Z1NzdYKgKnTiAF4E
This commit is contained in:
parent
5bf3e5d5e3
commit
2c85acb829
9 changed files with 539 additions and 10 deletions
73
tests/cli.rs
73
tests/cli.rs
|
|
@ -407,7 +407,7 @@ fn ownership_import_help_mentions_sync_preference_and_fallback_files() {
|
|||
.success()
|
||||
.stdout(predicate::str::contains("Prefer `idx ownership sync`"))
|
||||
.stdout(predicate::str::contains(
|
||||
"PDF (primary), ZIP/TXT archive (fallback)",
|
||||
"above-1% XLSX or PDF (primary), ZIP/TXT archive (fallback)",
|
||||
))
|
||||
.stdout(predicate::str::contains(
|
||||
"idx ownership import --file /path/to/BalanceposEfek20260227.zip",
|
||||
|
|
@ -1524,6 +1524,77 @@ fn ownership_import_url_duplicate_release_is_skipped() {
|
|||
.stdout(predicate::str::contains("Release already imported"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ownership_import_file_xlsx_supports_ticker_and_releases() {
|
||||
let root = test_env_dir("ownership-import-xlsx");
|
||||
let db_path = root.join("ownership.db");
|
||||
let xlsx_path = root.join("peng-2026-08-00017-satu-persen.xlsx");
|
||||
fs::write(
|
||||
&xlsx_path,
|
||||
include_bytes!("fixtures/ksei_above1_20260831_excerpt.xlsx"),
|
||||
)
|
||||
.expect("write xlsx fixture");
|
||||
|
||||
bin_with_root(&root)
|
||||
.args([
|
||||
"config",
|
||||
"set",
|
||||
"ownership.db_path",
|
||||
db_path.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
bin_with_root(&root)
|
||||
.args(["ownership", "import", "--file", xlsx_path.to_str().unwrap()])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("(as of 2026-08-31)"));
|
||||
|
||||
let output = bin_with_root(&root)
|
||||
.args(["-o", "json", "ownership", "ticker", "BBCA"])
|
||||
.output()
|
||||
.expect("ownership ticker json output");
|
||||
assert!(output.status.success());
|
||||
let body = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(body.contains("DWIMURIA INVESTAMA ANDALAN"), "{body}");
|
||||
|
||||
let output = bin_with_root(&root)
|
||||
.args(["-o", "json", "ownership", "releases"])
|
||||
.output()
|
||||
.expect("ownership releases json output");
|
||||
let releases: Value = serde_json::from_slice(&output.stdout).expect("parse releases json");
|
||||
assert_eq!(releases[0]["as_of_date"], "2026-08-31");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ownership_import_file_rejects_above5_xlsx() {
|
||||
let root = test_env_dir("ownership-import-xlsx-above5");
|
||||
let db_path = root.join("ownership.db");
|
||||
let xlsx_path = root.join("peng-2026-09-23-00080-lima-persen.xlsx");
|
||||
fs::write(
|
||||
&xlsx_path,
|
||||
include_bytes!("fixtures/ksei_above5_20260923_excerpt.xlsx"),
|
||||
)
|
||||
.expect("write xlsx fixture");
|
||||
|
||||
bin_with_root(&root)
|
||||
.args([
|
||||
"config",
|
||||
"set",
|
||||
"ownership.db_path",
|
||||
db_path.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
bin_with_root(&root)
|
||||
.args(["ownership", "import", "--file", xlsx_path.to_str().unwrap()])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains("above-1% holder register layout"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ownership_import_force_reimports_existing_release() {
|
||||
let root = test_env_dir("ownership-import-force");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue