use std::collections::HashMap;
use std::marker::PhantomData;
use tracing::{debug, info, warn};
use chrono::NaiveDate;
use crate::error::{FinTSError, Result};
use crate::message;
use crate::parser::{self, RawSegment, DEG};
use crate::segments::response::*;
use crate::transport::FinTSConnection;
use crate::types::*;
#[derive(Debug)]
pub struct New;
#[derive(Debug)]
pub struct Synced;
#[derive(Debug)]
pub struct Open;
#[derive(Debug)]
pub struct TanPending;
#[derive(Debug, Clone)]
pub(crate) enum Segment {
Identify { blz: Blz, user_id: UserId, system_id: SystemId },
ProcessPrep { bpd_version: u16, upd_version: u16, product_id: ProductId },
Sync,
Balance { account: Account },
Transactions {
account: Account,
start_date: NaiveDate,
end_date: NaiveDate,
touchdown: Option<TouchdownPoint>,
},
Holdings {
account: Account,
currency: Option<Currency>,
touchdown: Option<TouchdownPoint>,
},
TanProcess4 { reference_seg: SegmentRef, tan_medium: Option<TanMediumName> },
TanPollDecoupled { task_reference: TaskReference, tan_medium: Option<TanMediumName> },
TanProcess2 { task_reference: TaskReference, tan_medium: Option<TanMediumName> },
End { dialog_id: DialogId },
}
impl Segment {
pub(crate) fn to_degs(&self, params: &BankParams) -> Vec<DEG> {
use crate::segments::builder::*;
match self {
Segment::Identify { blz, user_id, system_id } => {
hkidn(0, blz.as_str(), user_id.as_str(), system_id.as_str())
}
Segment::ProcessPrep { bpd_version, upd_version, product_id } => {
hkvvb(0, *bpd_version, *upd_version, product_id.as_str())
}
Segment::Sync => {
hksyn(0)
}
Segment::Balance { account } => {
let version = params.supported_version("HISALS", 7).max(5);
hksal(0, version, account.iban(), account.bic(), None)
}
Segment::Transactions { account, start_date, end_date, touchdown } => {
let version = params.supported_version("HIKAZS", 7).max(5);
hkkaz(0, version, account.iban(), account.bic(), *start_date, *end_date, touchdown.as_ref().map(|t| t.as_str()))
}
Segment::Holdings { account, currency, touchdown } => {
let version = params.supported_version("HIWPDS", 7).max(1);
hkwpd(0, version, account.iban(), account.bic(), currency.as_ref().map(|c| c.as_str()), touchdown.as_ref().map(|t| t.as_str()))
}
Segment::TanProcess4 { reference_seg, tan_medium } => {
let version = params.hktan_version();
hktan_process4(0, version, reference_seg.as_str(), tan_medium.as_ref().map(|t| t.as_str()))
}
Segment::TanPollDecoupled { task_reference, tan_medium } => {
let version = params.hktan_version();
hktan_process_s(0, version, task_reference.as_str(), tan_medium.as_ref().map(|t| t.as_str()))
}
Segment::TanProcess2 { task_reference, tan_medium } => {
let version = params.hktan_version();
hktan_process2(0, version, task_reference.as_str(), tan_medium.as_ref().map(|t| t.as_str()))
}
Segment::End { dialog_id } => {
hkend(0, dialog_id.as_str())
}
}
}
}
pub enum InitResult {
Opened(Dialog<Open>, Response),
TanRequired(Dialog<TanPending>, TanChallenge, Response),
}
pub enum SendResult {
Success(Response),
NeedTan(Dialog<TanPending>, TanChallenge, Response),
Touchdown(Response, String),
}
pub enum PollResult {
Confirmed(Dialog<Open>, Response),
Pending(Dialog<TanPending>),
}
#[derive(Debug)]
pub struct Response {
pub segments: Vec<RawSegment>,
pub global_codes: Vec<ResponseCode>,
pub segment_codes: Vec<ResponseCode>,
}
impl Response {
pub fn find_segments(&self, seg_type: &str) -> Vec<&RawSegment> {
self.segments.iter().filter(|s| s.segment_type() == seg_type).collect()
}
pub fn find_segment(&self, seg_type: &str) -> Option<&RawSegment> {
self.segments.iter().find(|s| s.segment_type() == seg_type)
}
pub fn all_codes(&self) -> impl Iterator<Item = &ResponseCode> {
self.global_codes.iter().chain(self.segment_codes.iter())
}
pub fn needs_tan(&self) -> bool {
self.all_codes().any(|c| c.is_tan_required() || c.is_decoupled())
}
pub fn is_decoupled(&self) -> bool {
self.all_codes().any(|c| c.is_decoupled())
}
pub fn is_decoupled_pending(&self) -> bool {
self.all_codes().any(|c| c.is_decoupled_pending())
}
pub fn has_sca_exemption(&self) -> bool {
self.all_codes().any(|c| c.kind == ResponseCodeKind::ScaExemption)
}
pub fn touchdown(&self) -> Option<TouchdownPoint> {
find_touchdown(&self.segment_codes)
.or_else(|| find_touchdown(&self.global_codes))
}
pub fn get_tan_challenge(&self) -> Option<TanChallenge> {
if let Some(hitan) = self.find_segment("HITAN") {
let (task_ref, challenge, hhduc) = parse_hitan(hitan);
if !task_ref.is_empty() || !challenge.is_empty() {
return Some(TanChallenge {
challenge: ChallengeText::new(challenge),
challenge_hhduc: hhduc.map(HhdUcData),
task_reference: TaskReference::new(task_ref),
decoupled: self.is_decoupled(),
});
}
}
None
}
pub fn check_errors(&self) -> Result<()> {
for code in self.all_codes() {
match &code.kind {
ResponseCodeKind::PinWrong => return Err(FinTSError::PinWrong),
ResponseCodeKind::AccountLocked => return Err(FinTSError::AccountLocked),
k if k.is_error() => return Err(FinTSError::BankError {
kind: code.kind.clone(),
message: code.text.clone(),
}),
_ => {}
}
}
Ok(())
}
pub fn allowed_security_functions(&self) -> Vec<SecurityFunction> {
extract_allowed_security_functions(&self.segment_codes)
.into_iter()
.chain(extract_allowed_security_functions(&self.global_codes))
.collect()
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TanChallenge {
pub challenge: ChallengeText,
pub challenge_hhduc: Option<HhdUcData>,
pub task_reference: TaskReference,
pub decoupled: bool,
}
#[derive(Debug, Clone)]
pub struct BankParams {
pub bpd_version: u16,
pub upd_version: u16,
pub bpd_segments: Vec<RawSegment>,
pub upd_segments: Vec<RawSegment>,
pub tan_methods: Vec<TanMethod>,
pub selected_security_function: SecurityFunction,
pub selected_tan_medium: Option<TanMediumName>,
pub accounts_from_upd: Vec<SepaAccount>,
pub operation_tan_required: HashMap<SegmentType, bool>,
pub allowed_security_functions: Vec<SecurityFunction>,
pub preferred_security_function: Option<SecurityFunction>,
}
impl BankParams {
pub fn new() -> Self {
Self {
bpd_version: 0, upd_version: 0,
bpd_segments: Vec::new(), upd_segments: Vec::new(),
tan_methods: Vec::new(),
selected_security_function: SecurityFunction::pin_only(),
selected_tan_medium: None,
accounts_from_upd: Vec::new(),
operation_tan_required: HashMap::new(),
allowed_security_functions: Vec::new(),
preferred_security_function: None,
}
}
pub fn ingest_response(&mut self, response: &Response, system_id: &mut SystemId) {
for seg in &response.segments {
let stype = seg.segment_type();
match stype {
"HIBPA" => self.bpd_version = parse_hibpa_version(seg),
"HITANS" => self.tan_methods.extend(parse_hitans(seg)),
"HIPINS" => {
let m = parse_hipins(seg);
if !m.is_empty() {
info!("[FinTS] HIPINS: {} operation rules", m.len());
self.operation_tan_required.extend(m);
}
}
"HIUPA" => self.upd_version = parse_hiupa_version(seg),
"HIUPD" => {
self.upd_segments.push(seg.clone());
if let Some(acc) = parse_hiupd(seg) {
self.accounts_from_upd.push(acc);
}
}
"HISYN" => {
let sid = parse_hisyn_system_id(seg);
if !sid.is_empty() {
info!("[FinTS] System ID: {}", sid);
*system_id = SystemId::new(sid);
}
}
_ => {
if stype.starts_with("HI") && stype.len() >= 5 && stype.ends_with('S') {
self.bpd_segments.push(seg.clone());
}
}
}
}
let allowed = response.allowed_security_functions();
if !allowed.is_empty() {
self.allowed_security_functions = allowed;
}
}
pub fn needs_tan(&self, segment_type: &SegmentType) -> bool {
self.operation_tan_required.get(segment_type).copied().unwrap_or(true)
}
pub fn hktan_version(&self) -> u16 {
self.tan_methods.iter()
.find(|m| m.security_function == self.selected_security_function)
.map(|m| m.hktan_version)
.unwrap_or(7)
}
pub fn supported_version(&self, param_segment_type: &str, max_version: u16) -> u16 {
let v = find_highest_segment_version(&self.bpd_segments, param_segment_type, max_version);
let result = if v == 0 { max_version } else { v };
info!("[FinTS] BPD lookup: {} → found v{} (BPD has v{}, max={})",
param_segment_type, result, v, max_version);
result
}
pub fn select_security_function(&mut self) {
let allowed = &self.allowed_security_functions;
if allowed.is_empty() { return; }
if let Some(ref pref) = self.preferred_security_function {
if allowed.contains(pref) {
self.selected_security_function = pref.clone();
return;
}
}
let pin_only = SecurityFunction::pin_only();
let methods = &self.tan_methods;
let chosen = allowed.iter()
.filter(|sf| *sf != &pin_only)
.max_by_key(|sf| {
methods.iter().find(|m| &m.security_function == *sf)
.map(|m| if m.is_decoupled { 2i32 } else { 1 })
.unwrap_or(0)
});
if let Some(sf) = chosen {
info!("[FinTS] Selected security function: {}", sf);
self.selected_security_function = sf.clone();
}
}
pub fn is_decoupled(&self) -> bool {
self.tan_methods.iter()
.find(|m| m.security_function == self.selected_security_function)
.map(|m| m.is_decoupled)
.unwrap_or(false)
}
pub fn needs_tan_medium(&self) -> bool {
self.tan_methods.iter()
.find(|m| m.security_function == self.selected_security_function)
.map(|m| m.needs_tan_medium)
.unwrap_or(false)
}
pub fn decoupled_params(&self) -> (u64, u64, u32) {
self.tan_methods.iter()
.find(|m| m.security_function == self.selected_security_function)
.map(|m| {
let first = if m.wait_before_first_poll > 0 { m.wait_before_first_poll as u64 } else { 5 };
let next = if m.wait_before_next_poll > 0 { m.wait_before_next_poll as u64 } else { 5 };
let max = if m.decoupled_max_polls > 0 { m.decoupled_max_polls as u32 } else { 20 };
(first, next, max)
})
.unwrap_or((5, 5, 20))
}
}
pub struct Dialog<S: std::fmt::Debug> {
connection: FinTSConnection,
blz: Blz,
user_id: UserId,
pin: Pin,
system_id: SystemId,
product_id: ProductId,
dialog_id: DialogId,
message_number: u16,
pub params: BankParams,
_state: PhantomData<S>,
}
impl<S: std::fmt::Debug> std::fmt::Debug for Dialog<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Dialog")
.field("blz", &self.blz)
.field("user_id", &self.user_id)
.field("system_id", &self.system_id)
.field("dialog_id", &self.dialog_id)
.field("message_number", &self.message_number)
.field("state", &std::any::type_name::<S>())
.finish()
}
}
impl<S: std::fmt::Debug> Dialog<S> {
pub fn system_id(&self) -> &SystemId { &self.system_id }
pub fn bank_params(&self) -> &BankParams { &self.params }
pub fn bank_params_mut(&mut self) -> &mut BankParams { &mut self.params }
fn identify_segment(&self) -> Segment {
Segment::Identify {
blz: self.blz.clone(),
user_id: self.user_id.clone(),
system_id: self.system_id.clone(),
}
}
fn process_prep_segment(&self) -> Segment {
Segment::ProcessPrep {
bpd_version: self.params.bpd_version,
upd_version: self.params.upd_version,
product_id: self.product_id.clone(),
}
}
async fn send_segments(&mut self, segments: &[Segment]) -> Result<Response> {
let msg_bytes = message::build_message_from_typed(
&self.dialog_id, self.message_number,
&self.blz, &self.user_id, &self.system_id, &self.pin,
&self.params.selected_security_function,
segments, &self.params,
)?;
let msg_str = String::from_utf8_lossy(&msg_bytes);
let redacted = msg_str.replace(self.pin.as_str(), "***PIN***");
info!("[FinTS] Outgoing ({} bytes): {}", msg_bytes.len(), &redacted[..redacted.len().min(500)]);
self.message_number += 1;
let response_bytes = self.connection.send(&msg_bytes).await?;
parse_response(&response_bytes, self.message_number - 1)
}
async fn send_segments_with_tan(&mut self, segments: &[Segment], tan: &str) -> Result<Response> {
let msg_bytes = message::build_message_from_typed_with_tan(
&self.dialog_id, self.message_number,
&self.blz, &self.user_id, &self.system_id, &self.pin,
tan, &self.params.selected_security_function,
segments, &self.params,
)?;
self.message_number += 1;
let response_bytes = self.connection.send(&msg_bytes).await?;
parse_response(&response_bytes, self.message_number - 1)
}
async fn send_end(&mut self) -> Result<()> {
if !self.dialog_id.is_assigned() { return Ok(()); }
debug!("Ending dialog {}", self.dialog_id);
let msg_bytes = message::build_end_message(
&self.dialog_id, self.message_number,
&self.blz, &self.user_id, &self.system_id, &self.pin,
&self.params.selected_security_function,
&self.params,
)?;
self.message_number += 1;
let _ = self.connection.send(&msg_bytes).await;
self.dialog_id = DialogId::unassigned();
Ok(())
}
fn extract_dialog_id(&mut self, response: &Response) {
if let Some(hnhbk) = response.find_segment("HNHBK") {
let new_id = hnhbk.deg(3).get_str(0);
if !new_id.is_empty() && new_id != "0" {
self.dialog_id = DialogId::new(new_id);
}
}
}
fn transition<T: std::fmt::Debug>(self) -> Dialog<T> {
Dialog {
connection: self.connection, blz: self.blz, user_id: self.user_id,
pin: self.pin, system_id: self.system_id, product_id: self.product_id,
dialog_id: self.dialog_id, message_number: self.message_number,
params: self.params, _state: PhantomData,
}
}
}
impl Dialog<New> {
pub fn new(url: &str, blz: &Blz, user_id: &UserId, pin: &Pin, product_id: &ProductId) -> Result<Self> {
Ok(Self {
connection: FinTSConnection::new(url)?,
blz: blz.clone(), user_id: user_id.clone(),
pin: pin.clone(), system_id: SystemId::unassigned(),
product_id: product_id.clone(),
dialog_id: DialogId::unassigned(), message_number: 1,
params: BankParams::new(), _state: PhantomData,
})
}
pub fn with_system_id(mut self, system_id: &SystemId) -> Self {
self.system_id = system_id.clone(); self
}
pub fn with_params(mut self, params: &BankParams) -> Self {
self.params = params.clone(); self
}
pub fn with_tan_medium(mut self, medium: &TanMediumName) -> Self {
self.params.selected_tan_medium = Some(medium.clone()); self
}
pub async fn sync(mut self) -> Result<(Dialog<Synced>, Response)> {
info!("[FinTS] Sync dialog: BLZ={} user={} system_id={}", self.blz, self.user_id, self.system_id);
let segments = [
self.identify_segment(),
self.process_prep_segment(),
Segment::Sync,
];
let response = self.send_segments(&segments).await?;
self.extract_dialog_id(&response);
self.params.ingest_response(&response, &mut self.system_id);
self.params.select_security_function();
if !response.needs_tan() {
response.check_errors()?;
}
let bpd_summary: Vec<String> = self.params.bpd_segments.iter()
.map(|s| format!("{}:v{}", s.segment_type(), s.segment_version()))
.collect();
info!("[FinTS] Sync complete: BPD v{}, {} TAN methods, system_id={}",
self.params.bpd_version, self.params.tan_methods.len(), self.system_id);
info!("[FinTS] BPD segments ({}): {}", bpd_summary.len(), bpd_summary.join(", "));
Ok((self.transition(), response))
}
pub async fn init(mut self) -> Result<InitResult> {
let medium = self.params.selected_tan_medium.clone();
info!("[FinTS] Init dialog: BLZ={} security_fn={}", self.blz, self.params.selected_security_function);
let segments = [
self.identify_segment(),
self.process_prep_segment(),
Segment::TanProcess4 { reference_seg: SegmentRef::new("HKIDN"), tan_medium: medium },
];
let response = self.send_segments(&segments).await?;
self.extract_dialog_id(&response);
self.params.ingest_response(&response, &mut self.system_id);
let allowed = response.allowed_security_functions();
if !allowed.is_empty() {
self.params.allowed_security_functions = allowed;
self.params.select_security_function();
}
for c in response.all_codes() {
info!("[FinTS] Init: {} - {}", c.code(), c.text);
}
if response.needs_tan() {
if let Some(challenge) = response.get_tan_challenge() {
let challenge = TanChallenge {
decoupled: challenge.decoupled || self.params.is_decoupled(),
..challenge
};
info!("[FinTS] Init requires TAN: decoupled={}", challenge.decoupled);
return Ok(InitResult::TanRequired(self.transition(), challenge, response));
}
}
response.check_errors()?;
info!("[FinTS] Init opened without TAN");
Ok(InitResult::Opened(self.transition(), response))
}
pub async fn init_no_tan(mut self) -> Result<(Dialog<Open>, Response)> {
info!("[FinTS] Init (no HKTAN)");
let segments = [
self.identify_segment(),
self.process_prep_segment(),
];
let response = self.send_segments(&segments).await?;
self.extract_dialog_id(&response);
self.params.ingest_response(&response, &mut self.system_id);
response.check_errors()?;
Ok((self.transition(), response))
}
}
impl Dialog<Synced> {
pub async fn end(mut self) -> Result<(BankParams, SystemId)> {
self.send_end().await.ok();
Ok((self.params, self.system_id))
}
}
#[derive(Debug, Clone)]
pub struct Account {
iban: Iban,
bic: Bic,
}
impl Account {
pub fn new(iban: &str, bic: &str) -> Result<Self> {
if iban.is_empty() {
return Err(FinTSError::Dialog("IBAN must not be empty".into()));
}
if bic.is_empty() {
return Err(FinTSError::Dialog("BIC must not be empty. Please set the BIC in the account settings.".into()));
}
Ok(Self { iban: Iban::new(iban), bic: Bic::new(bic) })
}
pub fn iban(&self) -> &str { self.iban.as_str() }
pub fn bic(&self) -> &str { self.bic.as_str() }
}
pub enum BalanceResult {
Success(AccountBalance),
NeedTan(TanChallenge),
Empty,
}
pub struct TransactionPage {
pub booked: Mt940Data,
pub pending: Mt940Data,
pub touchdown: Option<TouchdownPoint>,
}
pub enum TransactionResult {
Success(TransactionPage),
NeedTan(TanChallenge),
}
pub struct HoldingsPage {
pub holdings: Vec<SecurityHolding>,
pub touchdown: Option<TouchdownPoint>,
}
pub enum HoldingsResult {
Success(HoldingsPage),
NeedTan(TanChallenge),
Empty,
}
impl Dialog<Open> {
pub async fn balance(&mut self, account: &Account) -> Result<BalanceResult> {
let hksal = SegmentType::new("HKSAL");
let needs_tan = self.params.needs_tan(&hksal);
let mut segments = vec![
Segment::Balance { account: account.clone() },
];
if needs_tan {
info!("[FinTS] balance: HKSAL + HKTAN:4 (HIPINS: TAN required)");
segments.push(Segment::TanProcess4 {
reference_seg: SegmentRef::new("HKSAL"),
tan_medium: self.params.selected_tan_medium.clone(),
});
} else {
info!("[FinTS] balance: HKSAL (HIPINS: PIN-only)");
}
let response = self.send_segments(&segments).await?;
for c in response.all_codes() {
if c.is_error() || c.is_warning() {
info!("[FinTS] HKSAL: {} - {}", c.code(), c.text);
}
}
if response.needs_tan() && !response.has_sca_exemption() {
if let Some(challenge) = response.get_tan_challenge() {
return Ok(BalanceResult::NeedTan(challenge));
}
}
response.check_errors()?;
if let Some(hisal) = response.find_segment("HISAL") {
if let Some(balance) = parse_hisal(hisal) {
return Ok(BalanceResult::Success(balance));
}
}
Ok(BalanceResult::Empty)
}
pub async fn transactions(
&mut self,
account: &Account,
start_date: NaiveDate,
end_date: NaiveDate,
touchdown: Option<&TouchdownPoint>,
) -> Result<TransactionResult> {
let is_first = touchdown.is_none();
let hkkaz = SegmentType::new("HKKAZ");
let needs_tan = self.params.needs_tan(&hkkaz);
let mut segments = vec![
Segment::Transactions {
account: account.clone(),
start_date,
end_date,
touchdown: touchdown.cloned(),
},
];
if is_first && needs_tan {
info!("[FinTS] transactions: HKKAZ + HKTAN:4 (HIPINS: TAN required)");
segments.push(Segment::TanProcess4 {
reference_seg: SegmentRef::new("HKKAZ"),
tan_medium: self.params.selected_tan_medium.clone(),
});
} else if is_first {
info!("[FinTS] transactions: HKKAZ (HIPINS: PIN-only)");
}
let response = self.send_segments(&segments).await?;
for c in response.all_codes() {
if c.is_error() || c.is_warning() {
info!("[FinTS] HKKAZ: {} - {}", c.code(), c.text);
}
}
if response.needs_tan() && !response.has_sca_exemption() {
if let Some(challenge) = response.get_tan_challenge() {
return Ok(TransactionResult::NeedTan(challenge));
}
}
response.check_errors()?;
let mt940 = extract_mt940_data(&response.segments);
let td = response.touchdown();
Ok(TransactionResult::Success(TransactionPage {
booked: Mt940Data(mt940.booked),
pending: Mt940Data(mt940.pending),
touchdown: td,
}))
}
pub async fn holdings(
&mut self,
account: &Account,
currency: Option<&Currency>,
touchdown: Option<&TouchdownPoint>,
) -> Result<HoldingsResult> {
let is_first = touchdown.is_none();
let hkwpd = SegmentType::new("HKWPD");
let needs_tan = self.params.needs_tan(&hkwpd);
let mut segments = vec![
Segment::Holdings {
account: account.clone(),
currency: currency.cloned(),
touchdown: touchdown.cloned(),
},
];
if is_first && needs_tan {
info!("[FinTS] holdings: HKWPD + HKTAN:4 (HIPINS: TAN required)");
segments.push(Segment::TanProcess4 {
reference_seg: SegmentRef::new("HKWPD"),
tan_medium: self.params.selected_tan_medium.clone(),
});
} else if is_first {
info!("[FinTS] holdings: HKWPD (HIPINS: PIN-only)");
}
let response = self.send_segments(&segments).await?;
for c in response.all_codes() {
if c.is_error() || c.is_warning() {
info!("[FinTS] HKWPD: {} - {}", c.code(), c.text);
}
}
if response.needs_tan() && !response.has_sca_exemption() {
if let Some(challenge) = response.get_tan_challenge() {
return Ok(HoldingsResult::NeedTan(challenge));
}
}
response.check_errors()?;
let holdings = parse_hiwpd(&response.segments);
let td = response.touchdown();
if holdings.is_empty() && td.is_none() {
return Ok(HoldingsResult::Empty);
}
Ok(HoldingsResult::Success(HoldingsPage {
holdings,
touchdown: td,
}))
}
pub async fn end(mut self) -> Result<()> {
self.send_end().await
}
}
impl Dialog<TanPending> {
pub async fn poll(mut self, task_reference: &TaskReference) -> Result<PollResult> {
let segments = [
Segment::TanPollDecoupled {
task_reference: task_reference.clone(),
tan_medium: self.params.selected_tan_medium.clone(),
},
];
let response = self.send_segments(&segments).await?;
for c in response.all_codes() {
info!("[FinTS] Poll: {} - {}", c.code(), c.text);
}
if response.is_decoupled_pending() {
return Ok(PollResult::Pending(self));
}
response.check_errors()?;
self.params.ingest_response(&response, &mut self.system_id);
Ok(PollResult::Confirmed(self.transition(), response))
}
pub async fn submit_tan(mut self, task_reference: &TaskReference, tan: &str) -> Result<(Dialog<Open>, Response)> {
let segments = [
Segment::TanProcess2 {
task_reference: task_reference.clone(),
tan_medium: self.params.selected_tan_medium.clone(),
},
];
let response = self.send_segments_with_tan(&segments, tan).await?;
response.check_errors()?;
self.params.ingest_response(&response, &mut self.system_id);
Ok((self.transition(), response))
}
pub async fn cancel(mut self) -> Result<()> {
self.send_end().await
}
}
fn parse_response(data: &[u8], expected_msg_num: u16) -> Result<Response> {
let outer_segments = parser::parse_message(data)?;
if let Some(hnhbk) = outer_segments.iter().find(|s| s.segment_type() == "HNHBK") {
let resp_num = hnhbk.deg(4).get_str(0);
let expected = expected_msg_num.to_string();
if resp_num != expected && !resp_num.is_empty() {
warn!("Message number mismatch: expected {}, got {}", expected, resp_num);
}
}
let mut all_segments = Vec::new();
for seg in &outer_segments {
if seg.segment_type() == "HNVSD" {
if let Some(binary) = seg.deg(1).get(0).as_bytes() {
match parser::parse_inner_segments(binary) {
Ok(inner) => all_segments.extend(inner),
Err(e) => warn!("Failed to parse HNVSD: {}", e),
}
}
} else {
all_segments.push(seg.clone());
}
}
let mut global_codes = Vec::new();
let mut segment_codes = Vec::new();
for seg in &all_segments {
match seg.segment_type() {
"HIRMG" => global_codes.extend(ResponseCode::parse_from_segment(seg)),
"HIRMS" => segment_codes.extend(ResponseCode::parse_from_segment(seg)),
_ => {}
}
}
Ok(Response { segments: all_segments, global_codes, segment_codes })
}