1use std::collections::HashMap;
42use std::marker::PhantomData;
43use tracing::{debug, info, warn};
44
45use chrono::NaiveDate;
46
47use crate::error::{FinTSError, Result};
48use crate::message;
49use crate::parser::{self, RawSegment, DEG};
50use crate::segments::response::*;
51use crate::transport::FinTSConnection;
52use crate::types::*;
53
54#[derive(Debug)]
60pub struct New;
61#[derive(Debug)]
63pub struct Synced;
64#[derive(Debug)]
66pub struct Open;
67#[derive(Debug)]
69pub struct TanPending;
70
71#[derive(Debug, Clone)]
79pub(crate) enum Segment {
80 Identify { blz: Blz, user_id: UserId, system_id: SystemId },
82 ProcessPrep { bpd_version: u16, upd_version: u16, product_id: ProductId },
84 Sync,
86 Balance { account: Account },
88 Transactions {
90 account: Account,
91 start_date: NaiveDate,
92 end_date: NaiveDate,
93 touchdown: Option<TouchdownPoint>,
94 },
95 Holdings {
97 account: Account,
98 currency: Option<Currency>,
99 touchdown: Option<TouchdownPoint>,
100 },
101 TanProcess4 { reference_seg: SegmentRef, tan_medium: Option<TanMediumName> },
103 TanPollDecoupled { task_reference: TaskReference, tan_medium: Option<TanMediumName> },
105 TanProcess2 { task_reference: TaskReference, tan_medium: Option<TanMediumName> },
107 End { dialog_id: DialogId },
109}
110
111impl Segment {
112 pub(crate) fn to_degs(&self, params: &BankParams) -> Vec<DEG> {
114 use crate::segments::builder::*;
115 match self {
116 Segment::Identify { blz, user_id, system_id } => {
117 hkidn(0, blz.as_str(), user_id.as_str(), system_id.as_str())
118 }
119 Segment::ProcessPrep { bpd_version, upd_version, product_id } => {
120 hkvvb(0, *bpd_version, *upd_version, product_id.as_str())
121 }
122 Segment::Sync => {
123 hksyn(0)
124 }
125 Segment::Balance { account } => {
126 let version = params.supported_version("HISALS", 7).max(5);
127 hksal(0, version, account.iban(), account.bic(), None)
128 }
129 Segment::Transactions { account, start_date, end_date, touchdown } => {
130 let version = params.supported_version("HIKAZS", 7).max(5);
131 hkkaz(0, version, account.iban(), account.bic(), *start_date, *end_date, touchdown.as_ref().map(|t| t.as_str()))
132 }
133 Segment::Holdings { account, currency, touchdown } => {
134 let version = params.supported_version("HIWPDS", 7).max(1);
135 hkwpd(0, version, account.iban(), account.bic(), currency.as_ref().map(|c| c.as_str()), touchdown.as_ref().map(|t| t.as_str()))
136 }
137 Segment::TanProcess4 { reference_seg, tan_medium } => {
138 let version = params.hktan_version();
139 hktan_process4(0, version, reference_seg.as_str(), tan_medium.as_ref().map(|t| t.as_str()))
140 }
141 Segment::TanPollDecoupled { task_reference, tan_medium } => {
142 let version = params.hktan_version();
143 hktan_process_s(0, version, task_reference.as_str(), tan_medium.as_ref().map(|t| t.as_str()))
144 }
145 Segment::TanProcess2 { task_reference, tan_medium } => {
146 let version = params.hktan_version();
147 hktan_process2(0, version, task_reference.as_str(), tan_medium.as_ref().map(|t| t.as_str()))
148 }
149 Segment::End { dialog_id } => {
150 hkend(0, dialog_id.as_str())
151 }
152 }
153 }
154}
155
156pub enum InitResult {
163 Opened(Dialog<Open>, Response),
165 TanRequired(Dialog<TanPending>, TanChallenge, Response),
167}
168
169pub enum SendResult {
175 Success(Response),
177 NeedTan(Dialog<TanPending>, TanChallenge, Response),
179 Touchdown(Response, String),
182}
183
184pub enum PollResult {
190 Confirmed(Dialog<Open>, Response),
192 Pending(Dialog<TanPending>),
194}
195
196#[derive(Debug)]
202pub struct Response {
203 pub segments: Vec<RawSegment>,
205 pub global_codes: Vec<ResponseCode>,
207 pub segment_codes: Vec<ResponseCode>,
209}
210
211impl Response {
212 pub fn find_segments(&self, seg_type: &str) -> Vec<&RawSegment> {
213 self.segments.iter().filter(|s| s.segment_type() == seg_type).collect()
214 }
215
216 pub fn find_segment(&self, seg_type: &str) -> Option<&RawSegment> {
217 self.segments.iter().find(|s| s.segment_type() == seg_type)
218 }
219
220 pub fn all_codes(&self) -> impl Iterator<Item = &ResponseCode> {
221 self.global_codes.iter().chain(self.segment_codes.iter())
222 }
223
224 pub fn needs_tan(&self) -> bool {
226 self.all_codes().any(|c| c.is_tan_required() || c.is_decoupled())
227 }
228
229 pub fn is_decoupled(&self) -> bool {
231 self.all_codes().any(|c| c.is_decoupled())
232 }
233
234 pub fn is_decoupled_pending(&self) -> bool {
236 self.all_codes().any(|c| c.is_decoupled_pending())
237 }
238
239 pub fn has_sca_exemption(&self) -> bool {
241 self.all_codes().any(|c| c.kind == ResponseCodeKind::ScaExemption)
242 }
243
244 pub fn touchdown(&self) -> Option<TouchdownPoint> {
247 find_touchdown(&self.segment_codes)
248 .or_else(|| find_touchdown(&self.global_codes))
249 }
250
251 pub fn get_tan_challenge(&self) -> Option<TanChallenge> {
253 if let Some(hitan) = self.find_segment("HITAN") {
254 let (task_ref, challenge, hhduc) = parse_hitan(hitan);
255 if !task_ref.is_empty() || !challenge.is_empty() {
256 return Some(TanChallenge {
257 challenge: ChallengeText::new(challenge),
258 challenge_hhduc: hhduc.map(HhdUcData),
259 task_reference: TaskReference::new(task_ref),
260 decoupled: self.is_decoupled(),
261 });
262 }
263 }
264 None
265 }
266
267 pub fn check_errors(&self) -> Result<()> {
269 for code in self.all_codes() {
270 match &code.kind {
271 ResponseCodeKind::PinWrong => return Err(FinTSError::PinWrong),
272 ResponseCodeKind::AccountLocked => return Err(FinTSError::AccountLocked),
273 k if k.is_error() => return Err(FinTSError::BankError {
274 kind: code.kind.clone(),
275 message: code.text.clone(),
276 }),
277 _ => {}
278 }
279 }
280 Ok(())
281 }
282
283 pub fn allowed_security_functions(&self) -> Vec<SecurityFunction> {
285 extract_allowed_security_functions(&self.segment_codes)
286 .into_iter()
287 .chain(extract_allowed_security_functions(&self.global_codes))
288 .collect()
289 }
290}
291
292#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
297pub struct TanChallenge {
298 pub challenge: ChallengeText,
299 pub challenge_hhduc: Option<HhdUcData>,
300 pub task_reference: TaskReference,
301 pub decoupled: bool,
302}
303
304#[derive(Debug, Clone)]
310pub struct BankParams {
311 pub bpd_version: u16,
312 pub upd_version: u16,
313 pub bpd_segments: Vec<RawSegment>,
314 pub upd_segments: Vec<RawSegment>,
315 pub tan_methods: Vec<TanMethod>,
316 pub selected_security_function: SecurityFunction,
317 pub selected_tan_medium: Option<TanMediumName>,
318 pub accounts_from_upd: Vec<SepaAccount>,
319 pub operation_tan_required: HashMap<SegmentType, bool>,
320 pub allowed_security_functions: Vec<SecurityFunction>,
321 pub preferred_security_function: Option<SecurityFunction>,
322}
323
324impl BankParams {
325 pub fn new() -> Self {
326 Self {
327 bpd_version: 0, upd_version: 0,
328 bpd_segments: Vec::new(), upd_segments: Vec::new(),
329 tan_methods: Vec::new(),
330 selected_security_function: SecurityFunction::pin_only(),
331 selected_tan_medium: None,
332 accounts_from_upd: Vec::new(),
333 operation_tan_required: HashMap::new(),
334 allowed_security_functions: Vec::new(),
335 preferred_security_function: None,
336 }
337 }
338
339 pub fn ingest_response(&mut self, response: &Response, system_id: &mut SystemId) {
341 for seg in &response.segments {
342 let stype = seg.segment_type();
343 match stype {
344 "HIBPA" => self.bpd_version = parse_hibpa_version(seg),
345 "HITANS" => self.tan_methods.extend(parse_hitans(seg)),
346 "HIPINS" => {
347 let m = parse_hipins(seg);
348 if !m.is_empty() {
349 info!("[FinTS] HIPINS: {} operation rules", m.len());
350 self.operation_tan_required.extend(m);
351 }
352 }
353 "HIUPA" => self.upd_version = parse_hiupa_version(seg),
354 "HIUPD" => {
355 self.upd_segments.push(seg.clone());
356 if let Some(acc) = parse_hiupd(seg) {
357 self.accounts_from_upd.push(acc);
358 }
359 }
360 "HISYN" => {
361 let sid = parse_hisyn_system_id(seg);
362 if !sid.is_empty() {
363 info!("[FinTS] System ID: {}", sid);
364 *system_id = SystemId::new(sid);
365 }
366 }
367 _ => {
368 if stype.starts_with("HI") && stype.len() >= 5 && stype.ends_with('S') {
369 self.bpd_segments.push(seg.clone());
370 }
371 }
372 }
373 }
374 let allowed = response.allowed_security_functions();
375 if !allowed.is_empty() {
376 self.allowed_security_functions = allowed;
377 }
378 }
379
380 pub fn needs_tan(&self, segment_type: &SegmentType) -> bool {
382 self.operation_tan_required.get(segment_type).copied().unwrap_or(true)
383 }
384
385 pub fn hktan_version(&self) -> u16 {
387 self.tan_methods.iter()
388 .find(|m| m.security_function == self.selected_security_function)
389 .map(|m| m.hktan_version)
390 .unwrap_or(7)
391 }
392
393 pub fn supported_version(&self, param_segment_type: &str, max_version: u16) -> u16 {
395 let v = find_highest_segment_version(&self.bpd_segments, param_segment_type, max_version);
396 let result = if v == 0 { max_version } else { v };
397 info!("[FinTS] BPD lookup: {} → found v{} (BPD has v{}, max={})",
398 param_segment_type, result, v, max_version);
399 result
400 }
401
402 pub fn select_security_function(&mut self) {
404 let allowed = &self.allowed_security_functions;
405 if allowed.is_empty() { return; }
406
407 if let Some(ref pref) = self.preferred_security_function {
408 if allowed.contains(pref) {
409 self.selected_security_function = pref.clone();
410 return;
411 }
412 }
413
414 let pin_only = SecurityFunction::pin_only();
415 let methods = &self.tan_methods;
416 let chosen = allowed.iter()
417 .filter(|sf| *sf != &pin_only)
418 .max_by_key(|sf| {
419 methods.iter().find(|m| &m.security_function == *sf)
420 .map(|m| if m.is_decoupled { 2i32 } else { 1 })
421 .unwrap_or(0)
422 });
423
424 if let Some(sf) = chosen {
425 info!("[FinTS] Selected security function: {}", sf);
426 self.selected_security_function = sf.clone();
427 }
428 }
429
430 pub fn is_decoupled(&self) -> bool {
431 self.tan_methods.iter()
432 .find(|m| m.security_function == self.selected_security_function)
433 .map(|m| m.is_decoupled)
434 .unwrap_or(false)
435 }
436
437 pub fn needs_tan_medium(&self) -> bool {
438 self.tan_methods.iter()
439 .find(|m| m.security_function == self.selected_security_function)
440 .map(|m| m.needs_tan_medium)
441 .unwrap_or(false)
442 }
443
444 pub fn decoupled_params(&self) -> (u64, u64, u32) {
445 self.tan_methods.iter()
446 .find(|m| m.security_function == self.selected_security_function)
447 .map(|m| {
448 let first = if m.wait_before_first_poll > 0 { m.wait_before_first_poll as u64 } else { 5 };
449 let next = if m.wait_before_next_poll > 0 { m.wait_before_next_poll as u64 } else { 5 };
450 let max = if m.decoupled_max_polls > 0 { m.decoupled_max_polls as u32 } else { 20 };
451 (first, next, max)
452 })
453 .unwrap_or((5, 5, 20))
454 }
455}
456
457pub struct Dialog<S: std::fmt::Debug> {
462 connection: FinTSConnection,
463 blz: Blz,
464 user_id: UserId,
465 pin: Pin,
466 system_id: SystemId,
467 product_id: ProductId,
468 dialog_id: DialogId,
469 message_number: u16,
470 pub params: BankParams,
471 _state: PhantomData<S>,
472}
473
474impl<S: std::fmt::Debug> std::fmt::Debug for Dialog<S> {
475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 f.debug_struct("Dialog")
477 .field("blz", &self.blz)
478 .field("user_id", &self.user_id)
479 .field("system_id", &self.system_id)
480 .field("dialog_id", &self.dialog_id)
481 .field("message_number", &self.message_number)
482 .field("state", &std::any::type_name::<S>())
483 .finish()
484 }
485}
486
487impl<S: std::fmt::Debug> Dialog<S> {
492 pub fn system_id(&self) -> &SystemId { &self.system_id }
493 pub fn bank_params(&self) -> &BankParams { &self.params }
494 pub fn bank_params_mut(&mut self) -> &mut BankParams { &mut self.params }
495
496 fn identify_segment(&self) -> Segment {
498 Segment::Identify {
499 blz: self.blz.clone(),
500 user_id: self.user_id.clone(),
501 system_id: self.system_id.clone(),
502 }
503 }
504
505 fn process_prep_segment(&self) -> Segment {
507 Segment::ProcessPrep {
508 bpd_version: self.params.bpd_version,
509 upd_version: self.params.upd_version,
510 product_id: self.product_id.clone(),
511 }
512 }
513
514 async fn send_segments(&mut self, segments: &[Segment]) -> Result<Response> {
516 let msg_bytes = message::build_message_from_typed(
517 &self.dialog_id, self.message_number,
518 &self.blz, &self.user_id, &self.system_id, &self.pin,
519 &self.params.selected_security_function,
520 segments, &self.params,
521 )?;
522
523 let msg_str = String::from_utf8_lossy(&msg_bytes);
524 let redacted = msg_str.replace(self.pin.as_str(), "***PIN***");
525 info!("[FinTS] Outgoing ({} bytes): {}", msg_bytes.len(), &redacted[..redacted.len().min(500)]);
526
527 self.message_number += 1;
528 let response_bytes = self.connection.send(&msg_bytes).await?;
529 parse_response(&response_bytes, self.message_number - 1)
530 }
531
532 async fn send_segments_with_tan(&mut self, segments: &[Segment], tan: &str) -> Result<Response> {
534 let msg_bytes = message::build_message_from_typed_with_tan(
535 &self.dialog_id, self.message_number,
536 &self.blz, &self.user_id, &self.system_id, &self.pin,
537 tan, &self.params.selected_security_function,
538 segments, &self.params,
539 )?;
540 self.message_number += 1;
541 let response_bytes = self.connection.send(&msg_bytes).await?;
542 parse_response(&response_bytes, self.message_number - 1)
543 }
544
545 async fn send_end(&mut self) -> Result<()> {
546 if !self.dialog_id.is_assigned() { return Ok(()); }
547 debug!("Ending dialog {}", self.dialog_id);
548 let msg_bytes = message::build_end_message(
549 &self.dialog_id, self.message_number,
550 &self.blz, &self.user_id, &self.system_id, &self.pin,
551 &self.params.selected_security_function,
552 &self.params,
553 )?;
554 self.message_number += 1;
555 let _ = self.connection.send(&msg_bytes).await;
556 self.dialog_id = DialogId::unassigned();
557 Ok(())
558 }
559
560 fn extract_dialog_id(&mut self, response: &Response) {
561 if let Some(hnhbk) = response.find_segment("HNHBK") {
562 let new_id = hnhbk.deg(3).get_str(0);
563 if !new_id.is_empty() && new_id != "0" {
564 self.dialog_id = DialogId::new(new_id);
565 }
566 }
567 }
568
569 fn transition<T: std::fmt::Debug>(self) -> Dialog<T> {
570 Dialog {
571 connection: self.connection, blz: self.blz, user_id: self.user_id,
572 pin: self.pin, system_id: self.system_id, product_id: self.product_id,
573 dialog_id: self.dialog_id, message_number: self.message_number,
574 params: self.params, _state: PhantomData,
575 }
576 }
577}
578
579impl Dialog<New> {
582 pub fn new(url: &str, blz: &Blz, user_id: &UserId, pin: &Pin, product_id: &ProductId) -> Result<Self> {
583 Ok(Self {
584 connection: FinTSConnection::new(url)?,
585 blz: blz.clone(), user_id: user_id.clone(),
586 pin: pin.clone(), system_id: SystemId::unassigned(),
587 product_id: product_id.clone(),
588 dialog_id: DialogId::unassigned(), message_number: 1,
589 params: BankParams::new(), _state: PhantomData,
590 })
591 }
592
593 pub fn with_system_id(mut self, system_id: &SystemId) -> Self {
594 self.system_id = system_id.clone(); self
595 }
596
597 pub fn with_params(mut self, params: &BankParams) -> Self {
598 self.params = params.clone(); self
599 }
600
601 pub fn with_tan_medium(mut self, medium: &TanMediumName) -> Self {
602 self.params.selected_tan_medium = Some(medium.clone()); self
603 }
604
605 pub async fn sync(mut self) -> Result<(Dialog<Synced>, Response)> {
611 info!("[FinTS] Sync dialog: BLZ={} user={} system_id={}", self.blz, self.user_id, self.system_id);
612
613 let segments = [
614 self.identify_segment(),
615 self.process_prep_segment(),
616 Segment::Sync,
617 ];
618
619 let response = self.send_segments(&segments).await?;
620 self.extract_dialog_id(&response);
621 self.params.ingest_response(&response, &mut self.system_id);
622 self.params.select_security_function();
623
624 if !response.needs_tan() {
626 response.check_errors()?;
627 }
628
629 let bpd_summary: Vec<String> = self.params.bpd_segments.iter()
631 .map(|s| format!("{}:v{}", s.segment_type(), s.segment_version()))
632 .collect();
633 info!("[FinTS] Sync complete: BPD v{}, {} TAN methods, system_id={}",
634 self.params.bpd_version, self.params.tan_methods.len(), self.system_id);
635 info!("[FinTS] BPD segments ({}): {}", bpd_summary.len(), bpd_summary.join(", "));
636
637 Ok((self.transition(), response))
638 }
639
640 pub async fn init(mut self) -> Result<InitResult> {
646 let medium = self.params.selected_tan_medium.clone();
647 info!("[FinTS] Init dialog: BLZ={} security_fn={}", self.blz, self.params.selected_security_function);
648
649 let segments = [
650 self.identify_segment(),
651 self.process_prep_segment(),
652 Segment::TanProcess4 { reference_seg: SegmentRef::new("HKIDN"), tan_medium: medium },
653 ];
654
655 let response = self.send_segments(&segments).await?;
656 self.extract_dialog_id(&response);
657 self.params.ingest_response(&response, &mut self.system_id);
658
659 let allowed = response.allowed_security_functions();
660 if !allowed.is_empty() {
661 self.params.allowed_security_functions = allowed;
662 self.params.select_security_function();
663 }
664
665 for c in response.all_codes() {
666 info!("[FinTS] Init: {} - {}", c.code(), c.text);
667 }
668
669 if response.needs_tan() {
671 if let Some(challenge) = response.get_tan_challenge() {
672 let challenge = TanChallenge {
674 decoupled: challenge.decoupled || self.params.is_decoupled(),
675 ..challenge
676 };
677 info!("[FinTS] Init requires TAN: decoupled={}", challenge.decoupled);
678 return Ok(InitResult::TanRequired(self.transition(), challenge, response));
679 }
680 }
681
682 response.check_errors()?;
684 info!("[FinTS] Init opened without TAN");
685 Ok(InitResult::Opened(self.transition(), response))
686 }
687
688 pub async fn init_no_tan(mut self) -> Result<(Dialog<Open>, Response)> {
690 info!("[FinTS] Init (no HKTAN)");
691 let segments = [
692 self.identify_segment(),
693 self.process_prep_segment(),
694 ];
695
696 let response = self.send_segments(&segments).await?;
697 self.extract_dialog_id(&response);
698 self.params.ingest_response(&response, &mut self.system_id);
699 response.check_errors()?;
700 Ok((self.transition(), response))
701 }
702}
703
704impl Dialog<Synced> {
707 pub async fn end(mut self) -> Result<(BankParams, SystemId)> {
709 self.send_end().await.ok();
710 Ok((self.params, self.system_id))
711 }
712}
713
714#[derive(Debug, Clone)]
736pub struct Account {
737 iban: Iban,
738 bic: Bic,
739}
740
741impl Account {
742 pub fn new(iban: &str, bic: &str) -> Result<Self> {
744 if iban.is_empty() {
745 return Err(FinTSError::Dialog("IBAN must not be empty".into()));
746 }
747 if bic.is_empty() {
748 return Err(FinTSError::Dialog("BIC must not be empty. Please set the BIC in the account settings.".into()));
749 }
750 Ok(Self { iban: Iban::new(iban), bic: Bic::new(bic) })
751 }
752
753 pub fn iban(&self) -> &str { self.iban.as_str() }
754 pub fn bic(&self) -> &str { self.bic.as_str() }
755}
756
757pub enum BalanceResult {
763 Success(AccountBalance),
765 NeedTan(TanChallenge),
767 Empty,
769}
770
771pub struct TransactionPage {
773 pub booked: Mt940Data,
775 pub pending: Mt940Data,
777 pub touchdown: Option<TouchdownPoint>,
779}
780
781pub enum TransactionResult {
783 Success(TransactionPage),
785 NeedTan(TanChallenge),
787}
788
789pub struct HoldingsPage {
791 pub holdings: Vec<SecurityHolding>,
793 pub touchdown: Option<TouchdownPoint>,
795}
796
797pub enum HoldingsResult {
799 Success(HoldingsPage),
801 NeedTan(TanChallenge),
803 Empty,
805}
806
807impl Dialog<Open> {
810 pub async fn balance(&mut self, account: &Account) -> Result<BalanceResult> {
816 let hksal = SegmentType::new("HKSAL");
817 let needs_tan = self.params.needs_tan(&hksal);
818 let mut segments = vec![
819 Segment::Balance { account: account.clone() },
820 ];
821 if needs_tan {
822 info!("[FinTS] balance: HKSAL + HKTAN:4 (HIPINS: TAN required)");
823 segments.push(Segment::TanProcess4 {
824 reference_seg: SegmentRef::new("HKSAL"),
825 tan_medium: self.params.selected_tan_medium.clone(),
826 });
827 } else {
828 info!("[FinTS] balance: HKSAL (HIPINS: PIN-only)");
829 }
830
831 let response = self.send_segments(&segments).await?;
832
833 for c in response.all_codes() {
834 if c.is_error() || c.is_warning() {
835 info!("[FinTS] HKSAL: {} - {}", c.code(), c.text);
836 }
837 }
838
839 if response.needs_tan() && !response.has_sca_exemption() {
841 if let Some(challenge) = response.get_tan_challenge() {
842 return Ok(BalanceResult::NeedTan(challenge));
843 }
844 }
845
846 response.check_errors()?;
848
849 if let Some(hisal) = response.find_segment("HISAL") {
851 if let Some(balance) = parse_hisal(hisal) {
852 return Ok(BalanceResult::Success(balance));
853 }
854 }
855
856 Ok(BalanceResult::Empty)
857 }
858
859 pub async fn transactions(
865 &mut self,
866 account: &Account,
867 start_date: NaiveDate,
868 end_date: NaiveDate,
869 touchdown: Option<&TouchdownPoint>,
870 ) -> Result<TransactionResult> {
871 let is_first = touchdown.is_none();
872 let hkkaz = SegmentType::new("HKKAZ");
873 let needs_tan = self.params.needs_tan(&hkkaz);
874
875 let mut segments = vec![
876 Segment::Transactions {
877 account: account.clone(),
878 start_date,
879 end_date,
880 touchdown: touchdown.cloned(),
881 },
882 ];
883
884 if is_first && needs_tan {
885 info!("[FinTS] transactions: HKKAZ + HKTAN:4 (HIPINS: TAN required)");
886 segments.push(Segment::TanProcess4 {
887 reference_seg: SegmentRef::new("HKKAZ"),
888 tan_medium: self.params.selected_tan_medium.clone(),
889 });
890 } else if is_first {
891 info!("[FinTS] transactions: HKKAZ (HIPINS: PIN-only)");
892 }
893
894 let response = self.send_segments(&segments).await?;
895
896 for c in response.all_codes() {
897 if c.is_error() || c.is_warning() {
898 info!("[FinTS] HKKAZ: {} - {}", c.code(), c.text);
899 }
900 }
901
902 if response.needs_tan() && !response.has_sca_exemption() {
904 if let Some(challenge) = response.get_tan_challenge() {
905 return Ok(TransactionResult::NeedTan(challenge));
906 }
907 }
908
909 response.check_errors()?;
911
912 let mt940 = extract_mt940_data(&response.segments);
914 let td = response.touchdown();
915
916 Ok(TransactionResult::Success(TransactionPage {
917 booked: Mt940Data(mt940.booked),
918 pending: Mt940Data(mt940.pending),
919 touchdown: td,
920 }))
921 }
922
923 pub async fn holdings(
930 &mut self,
931 account: &Account,
932 currency: Option<&Currency>,
933 touchdown: Option<&TouchdownPoint>,
934 ) -> Result<HoldingsResult> {
935 let is_first = touchdown.is_none();
936 let hkwpd = SegmentType::new("HKWPD");
937 let needs_tan = self.params.needs_tan(&hkwpd);
938
939 let mut segments = vec![
940 Segment::Holdings {
941 account: account.clone(),
942 currency: currency.cloned(),
943 touchdown: touchdown.cloned(),
944 },
945 ];
946
947 if is_first && needs_tan {
948 info!("[FinTS] holdings: HKWPD + HKTAN:4 (HIPINS: TAN required)");
949 segments.push(Segment::TanProcess4 {
950 reference_seg: SegmentRef::new("HKWPD"),
951 tan_medium: self.params.selected_tan_medium.clone(),
952 });
953 } else if is_first {
954 info!("[FinTS] holdings: HKWPD (HIPINS: PIN-only)");
955 }
956
957 let response = self.send_segments(&segments).await?;
958
959 for c in response.all_codes() {
960 if c.is_error() || c.is_warning() {
961 info!("[FinTS] HKWPD: {} - {}", c.code(), c.text);
962 }
963 }
964
965 if response.needs_tan() && !response.has_sca_exemption() {
967 if let Some(challenge) = response.get_tan_challenge() {
968 return Ok(HoldingsResult::NeedTan(challenge));
969 }
970 }
971
972 response.check_errors()?;
974
975 let holdings = parse_hiwpd(&response.segments);
977 let td = response.touchdown();
978
979 if holdings.is_empty() && td.is_none() {
980 return Ok(HoldingsResult::Empty);
981 }
982
983 Ok(HoldingsResult::Success(HoldingsPage {
984 holdings,
985 touchdown: td,
986 }))
987 }
988
989 pub async fn end(mut self) -> Result<()> {
991 self.send_end().await
992 }
993}
994
995impl Dialog<TanPending> {
998 pub async fn poll(mut self, task_reference: &TaskReference) -> Result<PollResult> {
1002 let segments = [
1003 Segment::TanPollDecoupled {
1004 task_reference: task_reference.clone(),
1005 tan_medium: self.params.selected_tan_medium.clone(),
1006 },
1007 ];
1008
1009 let response = self.send_segments(&segments).await?;
1010
1011 for c in response.all_codes() {
1012 info!("[FinTS] Poll: {} - {}", c.code(), c.text);
1013 }
1014
1015 if response.is_decoupled_pending() {
1017 return Ok(PollResult::Pending(self));
1018 }
1019
1020 response.check_errors()?;
1022
1023 self.params.ingest_response(&response, &mut self.system_id);
1025 Ok(PollResult::Confirmed(self.transition(), response))
1026 }
1027
1028 pub async fn submit_tan(mut self, task_reference: &TaskReference, tan: &str) -> Result<(Dialog<Open>, Response)> {
1031 let segments = [
1032 Segment::TanProcess2 {
1033 task_reference: task_reference.clone(),
1034 tan_medium: self.params.selected_tan_medium.clone(),
1035 },
1036 ];
1037
1038 let response = self.send_segments_with_tan(&segments, tan).await?;
1039 response.check_errors()?;
1040 self.params.ingest_response(&response, &mut self.system_id);
1041 Ok((self.transition(), response))
1042 }
1043
1044 pub async fn cancel(mut self) -> Result<()> {
1046 self.send_end().await
1047 }
1048}
1049
1050fn parse_response(data: &[u8], expected_msg_num: u16) -> Result<Response> {
1055 let outer_segments = parser::parse_message(data)?;
1056
1057 if let Some(hnhbk) = outer_segments.iter().find(|s| s.segment_type() == "HNHBK") {
1058 let resp_num = hnhbk.deg(4).get_str(0);
1059 let expected = expected_msg_num.to_string();
1060 if resp_num != expected && !resp_num.is_empty() {
1061 warn!("Message number mismatch: expected {}, got {}", expected, resp_num);
1062 }
1063 }
1064
1065 let mut all_segments = Vec::new();
1066 for seg in &outer_segments {
1067 if seg.segment_type() == "HNVSD" {
1068 if let Some(binary) = seg.deg(1).get(0).as_bytes() {
1069 match parser::parse_inner_segments(binary) {
1070 Ok(inner) => all_segments.extend(inner),
1071 Err(e) => warn!("Failed to parse HNVSD: {}", e),
1072 }
1073 }
1074 } else {
1075 all_segments.push(seg.clone());
1076 }
1077 }
1078
1079 let mut global_codes = Vec::new();
1080 let mut segment_codes = Vec::new();
1081 for seg in &all_segments {
1082 match seg.segment_type() {
1083 "HIRMG" => global_codes.extend(ResponseCode::parse_from_segment(seg)),
1084 "HIRMS" => segment_codes.extend(ResponseCode::parse_from_segment(seg)),
1085 _ => {}
1086 }
1087 }
1088
1089 Ok(Response { segments: all_segments, global_codes, segment_codes })
1090}