1use chrono::NaiveDate;
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11
12use crate::parser::{DataElement, RawSegment, DEG};
13
14macro_rules! newtype_string {
19 ($(#[$meta:meta])* $name:ident) => {
20 $(#[$meta])*
21 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22 pub struct $name(String);
23
24 impl $name {
25 pub fn new(s: impl Into<String>) -> Self { Self(s.into()) }
26 pub fn as_str(&self) -> &str { &self.0 }
27 }
28
29 impl fmt::Display for $name {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) }
31 }
32
33 impl AsRef<str> for $name {
34 fn as_ref(&self) -> &str { &self.0 }
35 }
36 };
37}
38
39newtype_string!(Blz);
41newtype_string!(UserId);
43newtype_string!(SystemId);
45newtype_string!(ProductId);
47newtype_string!(DialogId);
49newtype_string!(SecurityFunction);
51newtype_string!(TaskReference);
53newtype_string!(SegmentType);
55newtype_string!(TanMediumName);
57newtype_string!(TouchdownPoint);
59newtype_string!(SegmentRef);
61newtype_string!(Currency);
63newtype_string!(Iban);
65newtype_string!(Bic);
67newtype_string!(BankName);
69newtype_string!(FinTSUrl);
71newtype_string!(ChallengeText);
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct HhdUcData(pub Vec<u8>);
77
78#[derive(Debug, Clone)]
80pub struct Mt940Data(pub Vec<u8>);
81
82impl Mt940Data {
83 pub fn new() -> Self {
84 Self(Vec::new())
85 }
86 pub fn is_empty(&self) -> bool {
87 self.0.is_empty()
88 }
89 pub fn as_bytes(&self) -> &[u8] {
90 &self.0
91 }
92 pub fn extend(&mut self, data: Vec<u8>) {
93 if !self.0.is_empty() && !self.0.ends_with(b"\r\n") {
94 self.0.extend_from_slice(b"\r\n");
95 }
96 self.0.extend(data);
97 }
98}
99
100#[derive(Clone)]
102pub struct Pin(String);
103
104impl Pin {
105 pub fn new(s: impl Into<String>) -> Self {
106 Self(s.into())
107 }
108 pub fn as_str(&self) -> &str {
109 &self.0
110 }
111}
112
113impl fmt::Debug for Pin {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 f.write_str("Pin(****)")
116 }
117}
118
119impl fmt::Display for Pin {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.write_str("****")
122 }
123}
124
125impl SystemId {
128 pub fn unassigned() -> Self {
130 Self("0".into())
131 }
132 pub fn is_assigned(&self) -> bool {
133 !self.0.is_empty() && self.0 != "0"
134 }
135}
136
137impl DialogId {
138 pub fn unassigned() -> Self {
139 Self("0".into())
140 }
141 pub fn is_assigned(&self) -> bool {
142 !self.0.is_empty() && self.0 != "0"
143 }
144}
145
146impl SecurityFunction {
147 pub fn pin_only() -> Self {
149 Self("999".into())
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub enum TanProcess {
156 OneStep,
158 TwoStep,
160}
161
162impl TanProcess {
163 pub fn from_str_val(s: &str) -> Self {
164 match s {
165 "1" => TanProcess::OneStep,
166 _ => TanProcess::TwoStep,
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
174pub enum ResponseCodeKind {
175 MessageReceived,
177 OrderExecuted,
179 TanRequired,
181 DialogEnded,
183 TanValid,
185 Touchdown(TouchdownPoint),
187 PartialWarnings,
189 ScaExemption,
191 AllowedSecurityFunctions(Vec<SecurityFunction>),
193 DecoupledInitiated,
195 DecoupledPending,
197 GeneralError,
199 AuthenticationMissing,
201 PartialErrors,
203 UnexpectedInSync,
205 DataElementMissing,
207 PinWrong,
209 DialogAborted,
211 AccountLocked,
213 OtherSuccess(String),
215 OtherWarning(String),
217 OtherError(String),
219 Unknown(String),
221}
222
223impl ResponseCodeKind {
224 fn default_unknown() -> Self {
225 Self::Unknown(String::new())
226 }
227
228 pub fn from_code(code: &str, parameters: &[String]) -> Self {
229 match code {
230 "0010" => Self::MessageReceived,
231 "0020" => Self::OrderExecuted,
232 "0030" => Self::TanRequired,
233 "0100" => Self::DialogEnded,
234 "0900" => Self::TanValid,
235 "3040" => Self::Touchdown(TouchdownPoint::new(
236 parameters.first().map(|s| s.as_str()).unwrap_or(""),
237 )),
238 "3060" => Self::PartialWarnings,
239 "3076" => Self::ScaExemption,
240 "3920" => Self::AllowedSecurityFunctions(
241 parameters
242 .iter()
243 .map(|s| SecurityFunction::new(s))
244 .collect(),
245 ),
246 "3955" => Self::DecoupledInitiated,
247 "3956" => Self::DecoupledPending,
248 "9010" => Self::GeneralError,
249 "9040" => Self::AuthenticationMissing,
250 "9050" => Self::PartialErrors,
251 "9110" => Self::UnexpectedInSync,
252 "9160" => Self::DataElementMissing,
253 "9340" => Self::PinWrong,
254 "9800" => Self::DialogAborted,
255 "9942" => Self::AccountLocked,
256 _ if code.starts_with('0') => Self::OtherSuccess(code.to_string()),
257 _ if code.starts_with('3') => Self::OtherWarning(code.to_string()),
258 _ if code.starts_with('9') => Self::OtherError(code.to_string()),
259 _ => Self::Unknown(code.to_string()),
260 }
261 }
262
263 pub fn is_success(&self) -> bool {
264 matches!(
265 self,
266 Self::MessageReceived
267 | Self::OrderExecuted
268 | Self::TanRequired
269 | Self::DialogEnded
270 | Self::TanValid
271 | Self::OtherSuccess(_)
272 )
273 }
274
275 pub fn is_warning(&self) -> bool {
276 matches!(
277 self,
278 Self::Touchdown(_)
279 | Self::PartialWarnings
280 | Self::ScaExemption
281 | Self::AllowedSecurityFunctions(_)
282 | Self::DecoupledInitiated
283 | Self::DecoupledPending
284 | Self::OtherWarning(_)
285 )
286 }
287
288 pub fn is_error(&self) -> bool {
289 matches!(
290 self,
291 Self::GeneralError
292 | Self::AuthenticationMissing
293 | Self::PartialErrors
294 | Self::UnexpectedInSync
295 | Self::DataElementMissing
296 | Self::PinWrong
297 | Self::DialogAborted
298 | Self::AccountLocked
299 | Self::OtherError(_)
300 )
301 }
302}
303
304pub(crate) fn read_str(seg: &RawSegment, deg: usize, de: usize) -> String {
308 seg.deg(deg).get(de).as_text()
309}
310
311pub(crate) fn read_opt_str(seg: &RawSegment, deg: usize, de: usize) -> Option<String> {
313 let s = seg.deg(deg).get(de).as_text();
314 if s.is_empty() {
315 None
316 } else {
317 Some(s)
318 }
319}
320
321pub(crate) fn read_int(seg: &RawSegment, deg: usize, de: usize) -> i64 {
323 seg.deg(deg)
324 .get(de)
325 .as_str()
326 .and_then(|s| s.parse().ok())
327 .unwrap_or(0)
328}
329
330pub(crate) fn read_u16(seg: &RawSegment, deg: usize, de: usize) -> u16 {
332 seg.deg(deg)
333 .get(de)
334 .as_str()
335 .and_then(|s| s.parse().ok())
336 .unwrap_or(0)
337}
338
339pub(crate) fn read_date(seg: &RawSegment, deg: usize, de: usize) -> Option<NaiveDate> {
341 let s = seg.deg(deg).get(de).as_text();
342 if s.len() == 8 {
343 NaiveDate::parse_from_str(&s, "%Y%m%d").ok()
344 } else {
345 None
346 }
347}
348
349pub(crate) fn read_amount(seg: &RawSegment, deg: usize, de: usize) -> Option<Decimal> {
351 let s = seg.deg(deg).get(de).as_text();
352 if s.is_empty() {
353 return None;
354 }
355 let normalized = s.replace(',', ".");
356 Decimal::from_str(&normalized).ok()
357}
358
359pub(crate) fn read_binary(seg: &RawSegment, deg: usize, de: usize) -> Option<Vec<u8>> {
361 seg.deg(deg).get(de).as_bytes().map(|b| b.to_vec())
362}
363
364pub(crate) fn read_bool(seg: &RawSegment, deg: usize, de: usize) -> bool {
366 seg.deg(deg).get(de).as_text() == "J"
367}
368
369pub fn de_text(s: &str) -> DataElement {
373 if s.is_empty() {
374 DataElement::Empty
375 } else {
376 DataElement::Text(s.to_string())
377 }
378}
379
380pub fn de_num<T: ToString>(n: T) -> DataElement {
382 DataElement::Text(n.to_string())
383}
384
385pub fn de_date(date: NaiveDate) -> DataElement {
387 DataElement::Text(date.format("%Y%m%d").to_string())
388}
389
390pub fn de_empty() -> DataElement {
392 DataElement::Empty
393}
394
395pub fn de_binary(data: Vec<u8>) -> DataElement {
397 DataElement::Binary(data)
398}
399
400pub fn de_bool(val: bool) -> DataElement {
402 DataElement::Text(if val { "J" } else { "N" }.to_string())
403}
404
405pub fn deg(elements: Vec<DataElement>) -> DEG {
407 DEG(elements)
408}
409
410pub fn deg1(de: DataElement) -> DEG {
412 DEG(vec![de])
413}
414
415#[derive(Debug, Clone)]
420pub struct ResponseCode {
421 pub kind: ResponseCodeKind,
423 pub text: String,
425}
426
427impl ResponseCode {
428 pub fn new(code: &str, text: &str) -> Self {
430 Self {
431 kind: ResponseCodeKind::from_code(code, &[]),
432 text: text.to_string(),
433 }
434 }
435
436 pub fn with_params(code: &str, text: &str, params: Vec<String>) -> Self {
438 Self {
439 kind: ResponseCodeKind::from_code(code, ¶ms),
440 text: text.to_string(),
441 }
442 }
443
444 pub fn parse_from_segment(seg: &RawSegment) -> Vec<ResponseCode> {
446 let mut codes = Vec::new();
447 for i in 1..seg.deg_count() {
448 let d = seg.deg(i);
449 if d.len() >= 3 {
450 let code = d.get_str(0);
451 if code.is_empty() {
452 continue;
453 }
454 let text = d.get_str(2);
455 let mut params = Vec::new();
456 for j in 3..d.len() {
457 let p = d.get(j).as_text();
458 if !p.is_empty() {
459 params.push(p);
460 }
461 }
462 codes.push(ResponseCode {
463 kind: ResponseCodeKind::from_code(&code, ¶ms),
464 text,
465 });
466 }
467 }
468 codes
469 }
470
471 pub fn code(&self) -> &str {
473 match &self.kind {
474 ResponseCodeKind::MessageReceived => "0010",
475 ResponseCodeKind::OrderExecuted => "0020",
476 ResponseCodeKind::TanRequired => "0030",
477 ResponseCodeKind::DialogEnded => "0100",
478 ResponseCodeKind::TanValid => "0900",
479 ResponseCodeKind::Touchdown(_) => "3040",
480 ResponseCodeKind::PartialWarnings => "3060",
481 ResponseCodeKind::ScaExemption => "3076",
482 ResponseCodeKind::AllowedSecurityFunctions(_) => "3920",
483 ResponseCodeKind::DecoupledInitiated => "3955",
484 ResponseCodeKind::DecoupledPending => "3956",
485 ResponseCodeKind::GeneralError => "9010",
486 ResponseCodeKind::AuthenticationMissing => "9040",
487 ResponseCodeKind::PartialErrors => "9050",
488 ResponseCodeKind::UnexpectedInSync => "9110",
489 ResponseCodeKind::DataElementMissing => "9160",
490 ResponseCodeKind::PinWrong => "9340",
491 ResponseCodeKind::DialogAborted => "9800",
492 ResponseCodeKind::AccountLocked => "9942",
493 ResponseCodeKind::OtherSuccess(c)
494 | ResponseCodeKind::OtherWarning(c)
495 | ResponseCodeKind::OtherError(c)
496 | ResponseCodeKind::Unknown(c) => c,
497 }
498 }
499
500 pub fn is_success(&self) -> bool {
501 self.kind.is_success()
502 }
503 pub fn is_warning(&self) -> bool {
504 self.kind.is_warning()
505 }
506 pub fn is_error(&self) -> bool {
507 self.kind.is_error()
508 }
509 pub fn is_tan_required(&self) -> bool {
510 matches!(self.kind, ResponseCodeKind::TanRequired)
511 }
512 pub fn is_touchdown(&self) -> bool {
513 matches!(self.kind, ResponseCodeKind::Touchdown(_))
514 }
515 pub fn is_allowed_tan_methods(&self) -> bool {
516 matches!(self.kind, ResponseCodeKind::AllowedSecurityFunctions(_))
517 }
518 pub fn is_decoupled(&self) -> bool {
519 matches!(self.kind, ResponseCodeKind::DecoupledInitiated)
520 }
521 pub fn is_decoupled_pending(&self) -> bool {
522 matches!(self.kind, ResponseCodeKind::DecoupledPending)
523 }
524 pub fn is_pin_wrong(&self) -> bool {
525 matches!(self.kind, ResponseCodeKind::PinWrong)
526 }
527 pub fn is_general_error(&self) -> bool {
528 matches!(self.kind, ResponseCodeKind::GeneralError)
529 }
530 pub fn is_locked(&self) -> bool {
531 matches!(self.kind, ResponseCodeKind::AccountLocked)
532 }
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct TanMethod {
538 pub security_function: SecurityFunction,
540 pub tan_process: TanProcess,
542 pub name: String,
544 pub needs_tan_medium: bool,
546 pub decoupled_max_polls: i32,
548 pub wait_before_first_poll: i32,
550 pub wait_before_next_poll: i32,
552 pub is_decoupled: bool,
554 pub hktan_version: u16,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct SepaAccount {
561 pub iban: Iban,
562 pub bic: Bic,
563 pub account_number: String,
564 pub sub_account: String,
565 pub blz: Blz,
566 pub owner: Option<String>,
567 pub product_name: Option<String>,
568 pub currency: Option<Currency>,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct AccountBalance {
574 pub amount: Decimal,
575 pub date: NaiveDate,
576 pub currency: Currency,
577 pub credit_line: Option<Decimal>,
578 pub available: Option<Decimal>,
579 pub pending_amount: Option<Decimal>,
581 pub pending_date: Option<NaiveDate>,
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
587pub enum TransactionStatus {
588 Booked,
589 Pending,
590}
591
592newtype_string!(Isin);
596newtype_string!(Wkn);
598
599#[derive(Debug, Clone, Serialize, Deserialize)]
601pub struct SecurityHolding {
602 pub isin: Option<Isin>,
604 pub wkn: Option<Wkn>,
606 pub name: String,
608 pub quantity: Decimal,
610 pub price: Option<Decimal>,
612 pub price_currency: Option<Currency>,
614 pub price_date: Option<NaiveDate>,
616 pub market_value: Option<Decimal>,
618 pub market_value_currency: Option<Currency>,
620 pub acquisition_value: Option<Decimal>,
622 pub profit_loss: Option<Decimal>,
624 pub exchange: Option<String>,
626 pub depot_id: Option<String>,
628 pub raw: serde_json::Value,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct Transaction {
635 pub date: NaiveDate,
636 pub valuta_date: Option<NaiveDate>,
637 pub amount: Decimal,
638 pub currency: Currency,
639 pub applicant_name: Option<String>,
640 pub applicant_iban: Option<Iban>,
641 pub applicant_bic: Option<Bic>,
642 pub purpose: Option<String>,
643 pub posting_text: Option<String>,
644 pub reference: Option<String>,
645 pub raw: serde_json::Value,
646 pub status: TransactionStatus,
648}