use chrono::{Local, NaiveDate};
use crate::parser::{DataElement, DEG};
use crate::types::*;
struct Kti {
iban: String,
bic: String,
}
impl Kti {
fn new(iban: &str, bic: &str) -> Self {
assert!(!iban.is_empty(), "IBAN must not be empty in KTI DEG");
assert!(
!bic.is_empty(),
"BIC must not be empty in KTI DEG — use the bank's BIC as fallback"
);
Self {
iban: iban.to_string(),
bic: bic.to_string(),
}
}
fn to_deg(&self) -> DEG {
deg(vec![
DataElement::Text(self.iban.clone()),
DataElement::Text(self.bic.clone()),
])
}
}
pub(crate) fn seg_header(seg_type: &str, number: u16, version: u16) -> DEG {
deg(vec![de_text(seg_type), de_num(number), de_num(version)])
}
pub(crate) fn seg_header_ref(seg_type: &str, number: u16, version: u16, reference: u16) -> DEG {
deg(vec![
de_text(seg_type),
de_num(number),
de_num(version),
de_num(reference),
])
}
pub(crate) fn hnhbk(message_size: u32, dialog_id: &str, message_number: u16) -> Vec<DEG> {
vec![
seg_header("HNHBK", 1, 3),
deg1(de_text(&format!("{:012}", message_size))), deg1(de_text("300")), deg1(de_text(dialog_id)), deg1(de_num(message_number)), ]
}
pub(crate) fn hnhbs(segment_number: u16, message_number: u16) -> Vec<DEG> {
vec![
seg_header("HNHBS", segment_number, 1),
deg1(de_num(message_number)),
]
}
pub(crate) fn hnvsk(blz: &str, user_id: &str, system_id: &str) -> Vec<DEG> {
vec![
seg_header("HNVSK", 998, 3),
deg(vec![de_text("PIN"), de_text("1")]),
deg1(de_text("998")),
deg1(de_text("1")),
deg(vec![de_text("2"), de_empty(), de_text(system_id)]),
{
let now = Local::now();
deg(vec![
de_text("1"),
de_text(&now.format("%Y%m%d").to_string()),
de_text(&now.format("%H%M%S").to_string()),
])
},
deg(vec![
de_text("2"),
de_text("2"),
de_text("13"),
de_binary(vec![0u8; 8]),
de_text("5"),
de_text("1"),
]),
deg(vec![
de_text("280"),
de_text(blz),
de_text(user_id),
de_text("V"),
de_text("0"),
de_text("0"),
]),
deg1(de_text("0")),
]
}
pub(crate) fn hnvsd(inner_bytes: &[u8]) -> Vec<DEG> {
vec![
seg_header("HNVSD", 999, 1),
deg1(de_binary(inner_bytes.to_vec())),
]
}
pub(crate) fn hnshk(
segment_number: u16,
security_function: &str,
security_reference: u32,
blz: &str,
user_id: &str,
system_id: &str,
) -> Vec<DEG> {
vec![
seg_header("HNSHK", segment_number, 4),
deg(vec![de_text("PIN"), de_text("1")]),
deg1(de_text(security_function)),
deg1(de_num(security_reference)),
deg1(de_text("1")),
deg1(de_text("1")),
deg(vec![de_text("2"), de_empty(), de_text(system_id)]),
deg1(de_text("1")),
{
let now = Local::now();
deg(vec![
de_text("1"),
de_text(&now.format("%Y%m%d").to_string()),
de_text(&now.format("%H%M%S").to_string()),
])
},
deg(vec![de_text("1"), de_text("999"), de_text("1")]),
deg(vec![de_text("6"), de_text("10"), de_text("16")]),
deg(vec![
de_text("280"),
de_text(blz),
de_text(user_id),
de_text("S"),
de_text("0"),
de_text("0"),
]),
]
}
pub(crate) fn hnsha(
segment_number: u16,
security_reference: u32,
pin: &str,
tan: Option<&str>,
) -> Vec<DEG> {
let mut user_sig_elements = vec![de_text(pin)];
if let Some(t) = tan {
if t.is_empty() {
user_sig_elements.push(crate::parser::DataElement::Text(String::new()));
} else {
user_sig_elements.push(de_text(t));
}
}
vec![
seg_header("HNSHA", segment_number, 2),
deg1(de_num(security_reference)),
deg1(de_empty()),
deg(user_sig_elements),
]
}
pub(crate) fn hkidn(segment_number: u16, blz: &str, user_id: &str, system_id: &str) -> Vec<DEG> {
vec![
seg_header("HKIDN", segment_number, 2),
deg(vec![de_text("280"), de_text(blz)]),
deg1(de_text(user_id)),
deg1(de_text(system_id)),
deg1(de_text("1")),
]
}
pub(crate) fn hkvvb(
segment_number: u16,
bpd_version: u16,
upd_version: u16,
product_id: &str,
) -> Vec<DEG> {
vec![
seg_header("HKVVB", segment_number, 3),
deg1(de_num(bpd_version)),
deg1(de_num(upd_version)),
deg1(de_text("1")),
deg1(de_text(product_id)),
deg1(de_text("1.0")),
]
}
pub(crate) fn hksyn(segment_number: u16) -> Vec<DEG> {
vec![
seg_header("HKSYN", segment_number, 3),
deg1(de_text("0")),
]
}
pub(crate) fn hkend(segment_number: u16, dialog_id: &str) -> Vec<DEG> {
vec![
seg_header("HKEND", segment_number, 1),
deg1(de_text(dialog_id)),
]
}
pub(crate) fn hktan_process4(
segment_number: u16,
version: u16,
segment_type: &str,
tan_medium_name: Option<&str>,
) -> Vec<DEG> {
let mut degs = vec![
seg_header("HKTAN", segment_number, version),
deg1(de_text("4")),
deg1(de_text(segment_type)),
];
if version >= 6 {
degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); }
if let Some(name) = tan_medium_name {
if version >= 6 {
degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); }
degs.push(deg1(de_text(name)));
}
degs
}
pub(crate) fn hktan_process2(
segment_number: u16,
version: u16,
task_reference: &str,
tan_medium_name: Option<&str>,
) -> Vec<DEG> {
let mut degs = vec![
seg_header("HKTAN", segment_number, version),
deg1(de_text("2")),
];
if version >= 6 {
degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_text(task_reference))); degs.push(deg1(de_bool(false))); degs.push(deg1(de_empty())); } else {
degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_text(task_reference))); degs.push(deg1(de_bool(false))); }
if let Some(name) = tan_medium_name {
if version >= 6 {
degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); }
degs.push(deg1(de_text(name)));
}
degs
}
pub(crate) fn hktan_process_s(
segment_number: u16,
version: u16,
task_reference: &str,
tan_medium_name: Option<&str>,
) -> Vec<DEG> {
let mut degs = vec![
seg_header("HKTAN", segment_number, version),
deg1(de_text("S")),
];
if version >= 6 {
degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_empty())); degs.push(deg1(de_text(task_reference))); degs.push(deg1(de_bool(false))); degs.push(deg1(de_empty())); } else {
degs.push(deg1(de_empty()));
degs.push(deg1(de_empty()));
degs.push(deg1(de_text(task_reference)));
degs.push(deg1(de_bool(false)));
}
if let Some(name) = tan_medium_name {
if version >= 6 {
degs.push(deg1(de_empty()));
degs.push(deg1(de_empty()));
degs.push(deg1(de_empty()));
}
degs.push(deg1(de_text(name)));
}
degs
}
pub(crate) fn hktab(segment_number: u16, version: u16) -> Vec<DEG> {
vec![
seg_header("HKTAB", segment_number, version),
deg1(de_text("A")),
]
}
pub(crate) fn hkspa(segment_number: u16, version: u16) -> Vec<DEG> {
vec![seg_header("HKSPA", segment_number, version)]
}
pub(crate) fn hksal(
segment_number: u16,
version: u16,
iban: &str,
bic: &str,
touchdown: Option<&str>,
) -> Vec<DEG> {
let mut degs = if version >= 6 {
vec![
seg_header("HKSAL", segment_number, version),
Kti::new(iban, bic).to_deg(),
deg1(de_text("N")),
]
} else {
vec![
seg_header("HKSAL", segment_number, version),
deg(vec![
de_text(iban),
de_empty(),
de_text("280"),
de_text(bic),
]),
deg1(de_text("N")),
]
};
degs.push(deg1(de_empty()));
if let Some(td) = touchdown {
degs.push(deg1(de_text(td)));
}
degs
}
pub(crate) fn hkkaz(
segment_number: u16,
version: u16,
iban: &str,
bic: &str,
start_date: NaiveDate,
end_date: NaiveDate,
touchdown: Option<&str>,
) -> Vec<DEG> {
let mut degs = if version >= 6 {
vec![
seg_header("HKKAZ", segment_number, version),
Kti::new(iban, bic).to_deg(),
deg1(de_text("N")),
deg1(de_date(start_date)),
deg1(de_date(end_date)),
]
} else {
vec![
seg_header("HKKAZ", segment_number, version),
deg(vec![
de_text(iban),
de_empty(),
de_text("280"),
de_text(bic),
]),
deg1(de_text("N")),
deg1(de_date(start_date)),
deg1(de_date(end_date)),
]
};
degs.push(deg1(de_empty()));
if let Some(td) = touchdown {
degs.push(deg1(de_text(td)));
}
degs
}
pub(crate) fn hkwpd(
segment_number: u16,
version: u16,
iban: &str,
bic: &str,
currency: Option<&str>,
touchdown: Option<&str>,
) -> Vec<DEG> {
let mut degs = if version >= 6 {
vec![
seg_header("HKWPD", segment_number, version),
Kti::new(iban, bic).to_deg(),
]
} else {
vec![
seg_header("HKWPD", segment_number, version),
deg(vec![
de_text(iban),
de_empty(),
de_text("280"),
de_text(bic),
]),
]
};
degs.push(deg1(if let Some(cur) = currency {
de_text(cur)
} else {
de_empty()
}));
degs.push(deg1(de_empty()));
degs.push(deg1(de_empty()));
if let Some(td) = touchdown {
degs.push(deg1(de_text(td)));
}
degs
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serializer::{serialize_deg, serialize_segment};
#[test]
fn kti_produces_two_data_elements() {
let kti = Kti::new("DE04120300001084174299", "BYLADEM1001");
let deg = kti.to_deg();
assert_eq!(deg.0.len(), 2, "KTI DEG must have exactly 2 data elements");
assert_eq!(deg.get_str(0), "DE04120300001084174299");
assert_eq!(deg.get_str(1), "BYLADEM1001");
}
#[test]
fn kti_serializes_as_iban_colon_bic() {
let kti = Kti::new("DE04120300001084174299", "BYLADEM1001");
let bytes = serialize_deg(&kti.to_deg()).unwrap();
let wire = String::from_utf8(bytes).unwrap();
assert_eq!(wire, "DE04120300001084174299:BYLADEM1001");
}
#[test]
fn hksal_v7_wire_format() {
let degs = hksal(3, 7, "DE04120300001084174299", "BYLADEM1001", None);
let bytes = serialize_segment(°s).unwrap();
let wire = String::from_utf8(bytes).unwrap();
assert_eq!(wire, "HKSAL:3:7+DE04120300001084174299:BYLADEM1001+N'");
}
#[test]
fn hkkaz_v7_wire_format() {
let start = chrono::NaiveDate::from_ymd_opt(2025, 3, 29).unwrap();
let end = chrono::NaiveDate::from_ymd_opt(2026, 3, 29).unwrap();
let degs = hkkaz(
3,
7,
"DE04120300001084174299",
"BYLADEM1001",
start,
end,
None,
);
let bytes = serialize_segment(°s).unwrap();
let wire = String::from_utf8(bytes).unwrap();
assert_eq!(
wire,
"HKKAZ:3:7+DE04120300001084174299:BYLADEM1001+N+20250329+20260329'"
);
}
#[test]
fn hkwpd_v7_wire_format() {
let degs = hkwpd(3, 7, "DE04120300001084174299", "BYLADEM1001", None, None);
let bytes = serialize_segment(°s).unwrap();
let wire = String::from_utf8(bytes).unwrap();
assert_eq!(wire, "HKWPD:3:7+DE04120300001084174299:BYLADEM1001'");
}
#[test]
fn hkwpd_v7_with_currency() {
let degs = hkwpd(
3,
7,
"DE04120300001084174299",
"BYLADEM1001",
Some("EUR"),
None,
);
let bytes = serialize_segment(°s).unwrap();
let wire = String::from_utf8(bytes).unwrap();
assert_eq!(wire, "HKWPD:3:7+DE04120300001084174299:BYLADEM1001+EUR'");
}
#[test]
fn hkwpd_v7_with_touchdown() {
let degs = hkwpd(
3,
7,
"DE04120300001084174299",
"BYLADEM1001",
None,
Some("TOUCH123"),
);
let bytes = serialize_segment(°s).unwrap();
let wire = String::from_utf8(bytes).unwrap();
assert_eq!(
wire,
"HKWPD:3:7+DE04120300001084174299:BYLADEM1001++++TOUCH123'"
);
}
#[test]
#[should_panic(expected = "IBAN must not be empty")]
fn kti_panics_on_empty_iban() {
Kti::new("", "BYLADEM1001");
}
#[test]
#[should_panic(expected = "BIC must not be empty")]
fn kti_panics_on_empty_bic() {
Kti::new("DE04120300001084174299", "");
}
}