This commit is contained in:
parent
e652289ac4
commit
65aabf6381
|
|
@ -0,0 +1,3 @@
|
||||||
|
ANTARES_USERCODE=given_usercode
|
||||||
|
ANTARES_PASSWORD=given_password
|
||||||
|
OUT=out
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Copilot Instructions
|
||||||
|
|
||||||
|
## Build, test, and lint
|
||||||
|
|
||||||
|
- `cargo build`
|
||||||
|
- `cargo run` - requires a local `.env` file with `ANTARES_USERCODE` and `ANTARES_PASSWORD`
|
||||||
|
- `cargo test`
|
||||||
|
- `cargo test <test_name>` to run one test
|
||||||
|
- `cargo test -- --list` to discover test names; the repository currently has no defined tests
|
||||||
|
- `cargo fmt --all -- --check`
|
||||||
|
- `cargo clippy --all-targets -- -D warnings`
|
||||||
|
|
||||||
|
## High-level architecture
|
||||||
|
|
||||||
|
This is a modular single-binary Rust utility that fetches Antares B2B product data and exports it to Excel. The codebase is organized into separate concerns:
|
||||||
|
|
||||||
|
**Modules:**
|
||||||
|
- `src/main.rs` - orchestrates the pipeline: credential loading, API calls, JSON deserialization, and delegating to tools
|
||||||
|
- `src/api/client.rs` - handles URL construction for the Antares B2B API
|
||||||
|
- `src/tools/excel.rs` - implements Excel export with selective field mapping and business rule validation
|
||||||
|
- `src/tools/logger.rs` - provides dual output (terminal + file) logging to `log/{YYYY-MM-DD}.log`
|
||||||
|
- `src/template/antares.rs` - serde models with `#[serde(rename_all = "PascalCase")]`; top-level is `Antares` alias for `Vec<AntaresItem>`
|
||||||
|
|
||||||
|
**Pipeline:**
|
||||||
|
1. Initialize logger (creates `log/` dir if needed)
|
||||||
|
2. Load credentials from `.env` via `dotenv`
|
||||||
|
3. Build Antares API URL using `src/api/client::make_url()`
|
||||||
|
4. Fetch with 300-second timeout via blocking `reqwest::blocking::Client`
|
||||||
|
5. Deserialize into typed `Antares` structs; if schema mismatch, preserve response as JSON/text
|
||||||
|
6. Write `antares.json` with pretty formatting
|
||||||
|
7. Export filtered rows to `out/antares_export.xlsx` via `src/tools/excel::export_to_excel()`
|
||||||
|
8. Log success/failure at each step
|
||||||
|
|
||||||
|
**Logging:**
|
||||||
|
- All significant events logged to terminal and `log/{YYYY-MM-DD}.log`
|
||||||
|
- Specialized methods: `log_api_success()`, `log_api_failure()`, `log_export_success()`, `log_export_failure()`
|
||||||
|
- Directory created automatically if missing; logs appended daily by date
|
||||||
|
|
||||||
|
## Key conventions
|
||||||
|
|
||||||
|
**Credentials:**
|
||||||
|
- Load only via `ANTARES_USERCODE` and `ANTARES_PASSWORD` env vars from `.env`
|
||||||
|
- `.env.example` documents required variables
|
||||||
|
- No hardcoded credentials; missing vars cause graceful failure with instructions
|
||||||
|
|
||||||
|
**Artifacts:**
|
||||||
|
- `antares.json` - saved at repo root after each successful API fetch
|
||||||
|
- `out/antares_export.xlsx` - Excel export; `out/` directory created auto if missing
|
||||||
|
- `log/{YYYY-MM-DD}.log` - daily logs; `log/` directory created auto if missing
|
||||||
|
- `.gitignore` excludes: `*.json`, `*.xlsx`, `.env`, `/out`, `/log`
|
||||||
|
|
||||||
|
**Excel export (`src/tools/excel.rs`):**
|
||||||
|
- Not a generic dump; maps selected Antares fields to fixed column layout per business rules
|
||||||
|
- Rows skipped if `cikkszam` empty OR both name fields (`cikk_megnevezes_rovid`, `cikk_megnevezes`) empty
|
||||||
|
- `BESZCIKKNEV` prefers `cikk_megnevezes_rovid`, falls back to `cikk_megnevezes`
|
||||||
|
- `EGYSEGAR` prefers `netto_kisker_ar`; if `0.0`, searches `cikk_jellemzok` for entry with `jellemzo_nev == "Alap ár"`
|
||||||
|
- Headers are bold-formatted
|
||||||
|
|
||||||
|
**API integration:**
|
||||||
|
- When extending the schema, add/modify structs in `src/template/antares.rs` first
|
||||||
|
- Avoid ad hoc JSON traversal in `main.rs`; keep deserialization flow centered on typed structs
|
||||||
|
- Preserve response on schema mismatch (save as JSON or text) unless deliberately changing error handling
|
||||||
|
|
||||||
|
**Modularity:**
|
||||||
|
- Extend by adding new files to `src/tools/` (e.g., `csv.rs`, `database.rs`)
|
||||||
|
- Each tool should be self-contained and exposed via `src/tools/mod.rs`
|
||||||
|
- HTTP client logic stays in `src/api/`; reuse `make_request()` from `main.rs` for consistency
|
||||||
|
|
||||||
|
**Blocking HTTP:**
|
||||||
|
- Uses `reqwest::blocking::Client` with 300-second timeout
|
||||||
|
- No async runtime; synchronous flow is intentional
|
||||||
|
- Only change to async if architecture overhaul is deliberate
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
.DS_Store
|
||||||
/target
|
/target
|
||||||
*.json
|
*.json
|
||||||
*.xlsx
|
*.xlsx
|
||||||
|
.env
|
||||||
|
/out
|
||||||
|
/log
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,22 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "antares_get_data"
|
name = "antares_get_data"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"dotenv",
|
||||||
|
"log",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rust_xlsxwriter",
|
"rust_xlsxwriter",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -39,6 +51,12 @@ version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
|
|
@ -85,6 +103,19 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
@ -137,6 +168,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
|
@ -449,6 +486,30 @@ dependencies = [
|
||||||
"windows-registry",
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
|
@ -690,6 +751,15 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
|
|
@ -1460,6 +1530,41 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,7 @@ reqwest = { version = "0.12", features = ["blocking"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
rust_xlsxwriter = "0.66"
|
rust_xlsxwriter = "0.66"
|
||||||
|
dotenv = "0.15"
|
||||||
|
chrono = "0.4"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
pub fn make_url(base: &str, usercode: &str, password: &str, cikkszam: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{}&USERCODE={}&PASSWORD={}&PIN=&cikkszam={}",
|
||||||
|
base, usercode, password, cikkszam
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod client;
|
||||||
|
|
||||||
|
pub use client::make_url;
|
||||||
142
src/main.rs
142
src/main.rs
|
|
@ -1,102 +1,52 @@
|
||||||
|
use reqwest::blocking::Client;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use rust_xlsxwriter::*;
|
|
||||||
|
|
||||||
|
mod api;
|
||||||
mod template;
|
mod template;
|
||||||
|
mod tools;
|
||||||
|
|
||||||
|
use api::make_url;
|
||||||
use template::antares::Antares;
|
use template::antares::Antares;
|
||||||
|
use tools::{Logger, export_to_excel};
|
||||||
|
|
||||||
fn make_url(base: &str, usercode: &str, password: &str, cikkszam: &str) -> String {
|
fn make_request(url: &str) -> Result<String, Box<dyn Error>> {
|
||||||
format!(
|
let client = Client::builder()
|
||||||
"{}&USERCODE={}&PASSWORD={}&PIN=&cikkszam={}",
|
.timeout(Duration::from_secs(300))
|
||||||
base, usercode, password, cikkszam
|
.build()?;
|
||||||
)
|
Ok(client.get(url).send()?.text()?)
|
||||||
}
|
|
||||||
|
|
||||||
fn export_to_excel(items: &Antares, filename: &str) -> Result<(), Box<dyn Error>> {
|
|
||||||
let mut workbook = Workbook::new();
|
|
||||||
let sheet = workbook.add_worksheet();
|
|
||||||
|
|
||||||
let header_format = Format::new().set_bold();
|
|
||||||
|
|
||||||
// Write headers
|
|
||||||
sheet.write_string_with_format(0, 0, "BESZCIKKSZAM", &header_format)?;
|
|
||||||
sheet.write_string_with_format(0, 1, "GYCIKKSZAM", &header_format)?;
|
|
||||||
sheet.write_string_with_format(0, 2, "BESZCIKKNEV", &header_format)?;
|
|
||||||
sheet.write_string_with_format(0, 3, "GYARTO", &header_format)?;
|
|
||||||
sheet.write_string_with_format(0, 4, "CIKKAZON", &header_format)?;
|
|
||||||
sheet.write_string_with_format(0, 5, "KESZLET", &header_format)?;
|
|
||||||
sheet.write_string_with_format(0, 6, "ME", &header_format)?;
|
|
||||||
sheet.write_string_with_format(0, 7, "EGYSEGAR", &header_format)?;
|
|
||||||
|
|
||||||
let mut row = 1u32;
|
|
||||||
for item in items {
|
|
||||||
// Skip if BESZCIKKSZAM is empty
|
|
||||||
let cikkszam = match &item.cikkszam {
|
|
||||||
Some(cs) if !cs.is_empty() => cs.clone(),
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
// BESZCIKKNEV: cikk_megnevezes_rovid or cikk_megnevezes
|
|
||||||
let megnevezes = item
|
|
||||||
.cikk_megnevezes_rovid
|
|
||||||
.clone()
|
|
||||||
.or_else(|| item.cikk_megnevezes.clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Skip if BESZCIKKNEV is empty
|
|
||||||
if megnevezes.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EGYSEGAR: netto_kisker_ar or find in cikk_jellemzok where jellemzo_nev = "Alap ár"
|
|
||||||
let mut unit_price = item.netto_kisker_ar.unwrap_or(0.0);
|
|
||||||
if unit_price == 0.0 {
|
|
||||||
if let Some(ref jellemzok) = item.cikk_jellemzok {
|
|
||||||
for jellemzo in jellemzok {
|
|
||||||
if let Some(ref nev) = jellemzo.jellemzo_nev {
|
|
||||||
if nev == "Alap ár" {
|
|
||||||
if let Some(ref ertek) = jellemzo.jellemzo_ertek {
|
|
||||||
unit_price = ertek.parse().unwrap_or(0.0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write row data
|
|
||||||
sheet.write_string(row, 0, &cikkszam)?;
|
|
||||||
sheet.write_string(row, 1, &cikkszam)?;
|
|
||||||
sheet.write_string(row, 2, &megnevezes)?;
|
|
||||||
sheet.write_string(row, 3, "EGYEB")?;
|
|
||||||
sheet.write_string(row, 4, item.vonalkod.as_deref().unwrap_or(""))?;
|
|
||||||
sheet.write_number(row, 5, item.szabad_keszlet.unwrap_or(0) as f64)?;
|
|
||||||
sheet.write_string(row, 6, item.mennyisegi_egyseg_kod.as_deref().unwrap_or(""))?;
|
|
||||||
sheet.write_number(row, 7, unit_price)?;
|
|
||||||
|
|
||||||
row += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
workbook.save(filename)?;
|
|
||||||
println!("Excel file '{}' created with {} rows", filename, row - 1);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let logger = Logger::init()?;
|
||||||
|
logger.log_info("Starting Antares data export");
|
||||||
|
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
|
||||||
|
let usercode = std::env::var("ANTARES_USERCODE")
|
||||||
|
.map_err(|_| "Missing environment variable: ANTARES_USERCODE. Create a .env file with ANTARES_USERCODE, ANTARES_PASSWORD, and OUT. See .env.example for reference.")?;
|
||||||
|
let password = std::env::var("ANTARES_PASSWORD")
|
||||||
|
.map_err(|_| "Missing environment variable: ANTARES_PASSWORD. Create a .env file with ANTARES_USERCODE, ANTARES_PASSWORD, and OUT. See .env.example for reference.")?;
|
||||||
|
let out_path = std::env::var("OUT")
|
||||||
|
.map_err(|_| "Missing environment variable: OUT. Create a .env file with ANTARES_USERCODE, ANTARES_PASSWORD, and OUT. See .env.example for reference.")?;
|
||||||
|
|
||||||
// Separate values
|
// Separate values
|
||||||
let base = "https://b2b.antares.hu/I4stechproductionWebInt/IntAntaresWebCikkDataService.svc/webhttps/Get_CikkInfokWeb?SCHEMA=ANTARESINT";
|
let base = "https://b2b.antares.hu/I4stechproductionWebInt/IntAntaresWebCikkDataService.svc/webhttps/Get_CikkInfokWeb?SCHEMA=ANTARESINT";
|
||||||
let usercode = "orinkkft";
|
|
||||||
let password = "8E7DCB55F3B4ECC52D0451A1F8D851D0EF2193FCE4B83E528C18A8F68F8F2658EFCF2EA08563EC702A1C701934C3FBF5F6880BE894A16387326C7180A9A4C361";
|
|
||||||
let cikkszam = ""; // supply a value if needed
|
let cikkszam = ""; // supply a value if needed
|
||||||
|
|
||||||
let url = make_url(base, usercode, password, cikkszam);
|
let url = make_url(base, &usercode, &password, cikkszam);
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::builder()
|
let response = match make_request(&url) {
|
||||||
.timeout(Duration::from_secs(300))
|
Ok(resp) => {
|
||||||
.build()?;
|
logger.log_info("API request sent successfully");
|
||||||
let response = client.get(&url).send()?.text()?;
|
resp
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logger.log_api_failure(&e.to_string());
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Try to deserialize into strongly-typed `Antares` list.
|
// Try to deserialize into strongly-typed `Antares` list.
|
||||||
let items: Antares = match serde_json::from_str::<Antares>(&response) {
|
let items: Antares = match serde_json::from_str::<Antares>(&response) {
|
||||||
|
|
@ -104,7 +54,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
// Re-serialize via the structs to ensure consistent formatting
|
// Re-serialize via the structs to ensure consistent formatting
|
||||||
let pretty = serde_json::to_string_pretty(&items)?;
|
let pretty = serde_json::to_string_pretty(&items)?;
|
||||||
fs::write("antares.json", pretty)?;
|
fs::write("antares.json", pretty)?;
|
||||||
println!("Parsed {} items and saved to antares.json", items.len());
|
logger.log_api_success(items.len());
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
@ -112,28 +62,26 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(&response) {
|
if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(&response) {
|
||||||
let pretty = serde_json::to_string_pretty(&json_val)?;
|
let pretty = serde_json::to_string_pretty(&json_val)?;
|
||||||
fs::write("antares.json", pretty)?;
|
fs::write("antares.json", pretty)?;
|
||||||
eprintln!(
|
logger.log_info("Response saved as JSON (schema mismatch)");
|
||||||
"Warning: response didn't match expected struct but was valid JSON; saved pretty JSON to antares.json"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
fs::write("antares.json", &response)?;
|
fs::write("antares.json", &response)?;
|
||||||
eprintln!(
|
logger.log_info("Response saved as raw text (not valid JSON)");
|
||||||
"Warning: response is not valid JSON; saved raw response to antares.json"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export to Excel
|
// Export to Excel
|
||||||
export_to_excel(&items, "antares_export.xlsx")?;
|
match export_to_excel(&items, &out_path) {
|
||||||
|
Ok(_) => {
|
||||||
// Example: print first item's Cikkszam if available
|
logger.log_export_success(&out_path, items.len());
|
||||||
if let Some(first) = items.get(0) {
|
}
|
||||||
if let Some(ref cs) = first.cikkszam {
|
Err(e) => {
|
||||||
println!("First item Cikkszam: {}", cs);
|
logger.log_export_failure(&e.to_string());
|
||||||
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.log_info("Export process completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use rust_xlsxwriter::*;
|
||||||
|
use crate::template::antares::Antares;
|
||||||
|
|
||||||
|
fn resolve_excel_path(path_str: &str) -> Result<PathBuf, Box<dyn Error>> {
|
||||||
|
let path = Path::new(path_str);
|
||||||
|
|
||||||
|
// If path ends with .xlsx, treat it as a file path
|
||||||
|
if path_str.ends_with(".xlsx") {
|
||||||
|
// Create parent directories if needed
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
if !parent.as_os_str().is_empty() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(path.to_path_buf());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If path has an extension but it's not .xlsx, error out
|
||||||
|
if path.extension().is_some() {
|
||||||
|
return Err(format!(
|
||||||
|
"Invalid file extension. Expected .xlsx, got: {}",
|
||||||
|
path_str
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, treat as directory and create antares.xlsx inside
|
||||||
|
fs::create_dir_all(path)?;
|
||||||
|
Ok(path.join("antares.xlsx"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_to_excel(items: &Antares, path_str: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
let output_path = resolve_excel_path(path_str)?;
|
||||||
|
|
||||||
|
let mut workbook = Workbook::new();
|
||||||
|
let sheet = workbook.add_worksheet();
|
||||||
|
|
||||||
|
let header_format = Format::new().set_bold();
|
||||||
|
|
||||||
|
// Write headers
|
||||||
|
sheet.write_string_with_format(0, 0, "BESZCIKKSZAM", &header_format)?;
|
||||||
|
sheet.write_string_with_format(0, 1, "GYCIKKSZAM", &header_format)?;
|
||||||
|
sheet.write_string_with_format(0, 2, "BESZCIKKNEV", &header_format)?;
|
||||||
|
sheet.write_string_with_format(0, 3, "GYARTO", &header_format)?;
|
||||||
|
sheet.write_string_with_format(0, 4, "CIKKAZON", &header_format)?;
|
||||||
|
sheet.write_string_with_format(0, 5, "KESZLET", &header_format)?;
|
||||||
|
sheet.write_string_with_format(0, 6, "ME", &header_format)?;
|
||||||
|
sheet.write_string_with_format(0, 7, "EGYSEGAR", &header_format)?;
|
||||||
|
|
||||||
|
let mut row = 1u32;
|
||||||
|
for item in items {
|
||||||
|
// Skip if BESZCIKKSZAM is empty
|
||||||
|
let cikkszam = match &item.cikkszam {
|
||||||
|
Some(cs) if !cs.is_empty() => cs.clone(),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// BESZCIKKNEV: cikk_megnevezes_rovid or cikk_megnevezes
|
||||||
|
let megnevezes = item
|
||||||
|
.cikk_megnevezes_rovid
|
||||||
|
.clone()
|
||||||
|
.or_else(|| item.cikk_megnevezes.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Skip if BESZCIKKNEV is empty
|
||||||
|
if megnevezes.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EGYSEGAR: netto_kisker_ar or find in cikk_jellemzok where jellemzo_nev = "Alap ár"
|
||||||
|
let mut unit_price = item.netto_kisker_ar.unwrap_or(0.0);
|
||||||
|
if unit_price == 0.0 {
|
||||||
|
if let Some(ref jellemzok) = item.cikk_jellemzok {
|
||||||
|
for jellemzo in jellemzok {
|
||||||
|
if let Some(ref nev) = jellemzo.jellemzo_nev {
|
||||||
|
if nev == "Alap ár" {
|
||||||
|
if let Some(ref ertek) = jellemzo.jellemzo_ertek {
|
||||||
|
unit_price = ertek.parse().unwrap_or(0.0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write row data
|
||||||
|
sheet.write_string(row, 0, &cikkszam)?;
|
||||||
|
sheet.write_string(row, 1, &cikkszam)?;
|
||||||
|
sheet.write_string(row, 2, &megnevezes)?;
|
||||||
|
sheet.write_string(row, 3, "EGYEB")?;
|
||||||
|
sheet.write_string(row, 4, item.vonalkod.as_deref().unwrap_or(""))?;
|
||||||
|
sheet.write_number(row, 5, item.szabad_keszlet.unwrap_or(0) as f64)?;
|
||||||
|
sheet.write_string(row, 6, item.mennyisegi_egyseg_kod.as_deref().unwrap_or(""))?;
|
||||||
|
sheet.write_number(row, 7, unit_price)?;
|
||||||
|
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
workbook.save(&output_path)?;
|
||||||
|
let display_path = output_path.display();
|
||||||
|
println!("Excel file '{}' created with {} rows", display_path, row - 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
|
pub struct Logger {
|
||||||
|
log_file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
pub fn init() -> std::io::Result<Self> {
|
||||||
|
let log_dir = PathBuf::from("log");
|
||||||
|
fs::create_dir_all(&log_dir)?;
|
||||||
|
|
||||||
|
let today = Local::now().format("%Y-%m-%d").to_string();
|
||||||
|
let log_file = log_dir.join(format!("{}.log", today));
|
||||||
|
|
||||||
|
Ok(Logger { log_file })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log(&self, message: &str) {
|
||||||
|
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
let formatted = format!("[{}] {}", timestamp, message);
|
||||||
|
|
||||||
|
println!("{}", formatted);
|
||||||
|
|
||||||
|
if let Ok(mut file) = fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&self.log_file)
|
||||||
|
{
|
||||||
|
let _ = writeln!(file, "{}", formatted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_api_success(&self, item_count: usize) {
|
||||||
|
self.log(&format!(
|
||||||
|
"✓ API call successful - Fetched {} items",
|
||||||
|
item_count
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_api_failure(&self, error: &str) {
|
||||||
|
self.log(&format!("✗ API call failed: {}", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_export_success(&self, filepath: &str, row_count: usize) {
|
||||||
|
self.log(&format!(
|
||||||
|
"✓ Export successful - {} rows exported to {}",
|
||||||
|
row_count, filepath
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_export_failure(&self, error: &str) {
|
||||||
|
self.log(&format!("✗ Export failed: {}", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_info(&self, message: &str) {
|
||||||
|
self.log(&format!("ℹ {}", message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod excel;
|
||||||
|
pub mod logger;
|
||||||
|
|
||||||
|
pub use excel::export_to_excel;
|
||||||
|
pub use logger::Logger;
|
||||||
Loading…
Reference in New Issue