use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use crate::parser::{DataElement, RawSegment, DEG};
macro_rules! newtype_string {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct $name(String);
impl $name {
pub fn new(s: impl Into<String>) -> Self { Self(s.into()) }
pub fn as_str(&self) -> &str { &self.0 }
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) }
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str { &self.0 }
}
};
}
newtype_string!( Blz);
newtype_string!( UserId);
newtype_string!( SystemId);
newtype_string!( ProductId);
newtype_string!( DialogId);
newtype_string!( SecurityFunction);
newtype_string!( TaskReference);
newtype_string!( SegmentType);
newtype_string!( TanMediumName);
newtype_string!( TouchdownPoint);
newtype_string!( SegmentRef);
newtype_string!( Currency);
newtype_string!( Iban);
newtype_string!( Bic);
newtype_string!( BankName);
newtype_string!( FinTSUrl);
newtype_string!( ChallengeText);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HhdUcData(pub Vec<u8>);
#[derive(Debug, Clone)]
pub struct Mt940Data(pub Vec<u8>);
impl Mt940Data {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn extend(&mut self, data: Vec<u8>) {
if !self.0.is_empty() && !self.0.ends_with(b"\r\n") {
self.0.extend_from_slice(b"\r\n");
}
self.0.extend(data);
}
}
#[derive(Clone)]
pub struct Pin(String);
impl Pin {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for Pin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Pin(****)")
}
}
impl fmt::Display for Pin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("****")
}
}
impl SystemId {
pub fn unassigned() -> Self {
Self("0".into())
}
pub fn is_assigned(&self) -> bool {
!self.0.is_empty() && self.0 != "0"
}
}
impl DialogId {
pub fn unassigned() -> Self {
Self("0".into())
}
pub fn is_assigned(&self) -> bool {
!self.0.is_empty() && self.0 != "0"
}
}
impl SecurityFunction {
pub fn pin_only() -> Self {
Self("999".into())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TanProcess {
OneStep,
TwoStep,
}
impl TanProcess {
pub fn from_str_val(s: &str) -> Self {
match s {
"1" => TanProcess::OneStep,
_ => TanProcess::TwoStep,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponseCodeKind {
MessageReceived,
OrderExecuted,
TanRequired,
DialogEnded,
TanValid,
Touchdown(TouchdownPoint),
PartialWarnings,
ScaExemption,
AllowedSecurityFunctions(Vec<SecurityFunction>),
DecoupledInitiated,
DecoupledPending,
GeneralError,
AuthenticationMissing,
PartialErrors,
UnexpectedInSync,
DataElementMissing,
PinWrong,
DialogAborted,
AccountLocked,
OtherSuccess(String),
OtherWarning(String),
OtherError(String),
Unknown(String),
}
impl ResponseCodeKind {
fn default_unknown() -> Self {
Self::Unknown(String::new())
}
pub fn from_code(code: &str, parameters: &[String]) -> Self {
match code {
"0010" => Self::MessageReceived,
"0020" => Self::OrderExecuted,
"0030" => Self::TanRequired,
"0100" => Self::DialogEnded,
"0900" => Self::TanValid,
"3040" => Self::Touchdown(TouchdownPoint::new(
parameters.first().map(|s| s.as_str()).unwrap_or(""),
)),
"3060" => Self::PartialWarnings,
"3076" => Self::ScaExemption,
"3920" => Self::AllowedSecurityFunctions(
parameters
.iter()
.map(|s| SecurityFunction::new(s))
.collect(),
),
"3955" => Self::DecoupledInitiated,
"3956" => Self::DecoupledPending,
"9010" => Self::GeneralError,
"9040" => Self::AuthenticationMissing,
"9050" => Self::PartialErrors,
"9110" => Self::UnexpectedInSync,
"9160" => Self::DataElementMissing,
"9340" => Self::PinWrong,
"9800" => Self::DialogAborted,
"9942" => Self::AccountLocked,
_ if code.starts_with('0') => Self::OtherSuccess(code.to_string()),
_ if code.starts_with('3') => Self::OtherWarning(code.to_string()),
_ if code.starts_with('9') => Self::OtherError(code.to_string()),
_ => Self::Unknown(code.to_string()),
}
}
pub fn is_success(&self) -> bool {
matches!(
self,
Self::MessageReceived
| Self::OrderExecuted
| Self::TanRequired
| Self::DialogEnded
| Self::TanValid
| Self::OtherSuccess(_)
)
}
pub fn is_warning(&self) -> bool {
matches!(
self,
Self::Touchdown(_)
| Self::PartialWarnings
| Self::ScaExemption
| Self::AllowedSecurityFunctions(_)
| Self::DecoupledInitiated
| Self::DecoupledPending
| Self::OtherWarning(_)
)
}
pub fn is_error(&self) -> bool {
matches!(
self,
Self::GeneralError
| Self::AuthenticationMissing
| Self::PartialErrors
| Self::UnexpectedInSync
| Self::DataElementMissing
| Self::PinWrong
| Self::DialogAborted
| Self::AccountLocked
| Self::OtherError(_)
)
}
}
pub(crate) fn read_str(seg: &RawSegment, deg: usize, de: usize) -> String {
seg.deg(deg).get(de).as_text()
}
pub(crate) fn read_opt_str(seg: &RawSegment, deg: usize, de: usize) -> Option<String> {
let s = seg.deg(deg).get(de).as_text();
if s.is_empty() {
None
} else {
Some(s)
}
}
pub(crate) fn read_int(seg: &RawSegment, deg: usize, de: usize) -> i64 {
seg.deg(deg)
.get(de)
.as_str()
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
pub(crate) fn read_u16(seg: &RawSegment, deg: usize, de: usize) -> u16 {
seg.deg(deg)
.get(de)
.as_str()
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
pub(crate) fn read_date(seg: &RawSegment, deg: usize, de: usize) -> Option<NaiveDate> {
let s = seg.deg(deg).get(de).as_text();
if s.len() == 8 {
NaiveDate::parse_from_str(&s, "%Y%m%d").ok()
} else {
None
}
}
pub(crate) fn read_amount(seg: &RawSegment, deg: usize, de: usize) -> Option<Decimal> {
let s = seg.deg(deg).get(de).as_text();
if s.is_empty() {
return None;
}
let normalized = s.replace(',', ".");
Decimal::from_str(&normalized).ok()
}
pub(crate) fn read_binary(seg: &RawSegment, deg: usize, de: usize) -> Option<Vec<u8>> {
seg.deg(deg).get(de).as_bytes().map(|b| b.to_vec())
}
pub(crate) fn read_bool(seg: &RawSegment, deg: usize, de: usize) -> bool {
seg.deg(deg).get(de).as_text() == "J"
}
pub fn de_text(s: &str) -> DataElement {
if s.is_empty() {
DataElement::Empty
} else {
DataElement::Text(s.to_string())
}
}
pub fn de_num<T: ToString>(n: T) -> DataElement {
DataElement::Text(n.to_string())
}
pub fn de_date(date: NaiveDate) -> DataElement {
DataElement::Text(date.format("%Y%m%d").to_string())
}
pub fn de_empty() -> DataElement {
DataElement::Empty
}
pub fn de_binary(data: Vec<u8>) -> DataElement {
DataElement::Binary(data)
}
pub fn de_bool(val: bool) -> DataElement {
DataElement::Text(if val { "J" } else { "N" }.to_string())
}
pub fn deg(elements: Vec<DataElement>) -> DEG {
DEG(elements)
}
pub fn deg1(de: DataElement) -> DEG {
DEG(vec![de])
}
#[derive(Debug, Clone)]
pub struct ResponseCode {
pub kind: ResponseCodeKind,
pub text: String,
}
impl ResponseCode {
pub fn new(code: &str, text: &str) -> Self {
Self {
kind: ResponseCodeKind::from_code(code, &[]),
text: text.to_string(),
}
}
pub fn with_params(code: &str, text: &str, params: Vec<String>) -> Self {
Self {
kind: ResponseCodeKind::from_code(code, ¶ms),
text: text.to_string(),
}
}
pub fn parse_from_segment(seg: &RawSegment) -> Vec<ResponseCode> {
let mut codes = Vec::new();
for i in 1..seg.deg_count() {
let d = seg.deg(i);
if d.len() >= 3 {
let code = d.get_str(0);
if code.is_empty() {
continue;
}
let text = d.get_str(2);
let mut params = Vec::new();
for j in 3..d.len() {
let p = d.get(j).as_text();
if !p.is_empty() {
params.push(p);
}
}
codes.push(ResponseCode {
kind: ResponseCodeKind::from_code(&code, ¶ms),
text,
});
}
}
codes
}
pub fn code(&self) -> &str {
match &self.kind {
ResponseCodeKind::MessageReceived => "0010",
ResponseCodeKind::OrderExecuted => "0020",
ResponseCodeKind::TanRequired => "0030",
ResponseCodeKind::DialogEnded => "0100",
ResponseCodeKind::TanValid => "0900",
ResponseCodeKind::Touchdown(_) => "3040",
ResponseCodeKind::PartialWarnings => "3060",
ResponseCodeKind::ScaExemption => "3076",
ResponseCodeKind::AllowedSecurityFunctions(_) => "3920",
ResponseCodeKind::DecoupledInitiated => "3955",
ResponseCodeKind::DecoupledPending => "3956",
ResponseCodeKind::GeneralError => "9010",
ResponseCodeKind::AuthenticationMissing => "9040",
ResponseCodeKind::PartialErrors => "9050",
ResponseCodeKind::UnexpectedInSync => "9110",
ResponseCodeKind::DataElementMissing => "9160",
ResponseCodeKind::PinWrong => "9340",
ResponseCodeKind::DialogAborted => "9800",
ResponseCodeKind::AccountLocked => "9942",
ResponseCodeKind::OtherSuccess(c)
| ResponseCodeKind::OtherWarning(c)
| ResponseCodeKind::OtherError(c)
| ResponseCodeKind::Unknown(c) => c,
}
}
pub fn is_success(&self) -> bool {
self.kind.is_success()
}
pub fn is_warning(&self) -> bool {
self.kind.is_warning()
}
pub fn is_error(&self) -> bool {
self.kind.is_error()
}
pub fn is_tan_required(&self) -> bool {
matches!(self.kind, ResponseCodeKind::TanRequired)
}
pub fn is_touchdown(&self) -> bool {
matches!(self.kind, ResponseCodeKind::Touchdown(_))
}
pub fn is_allowed_tan_methods(&self) -> bool {
matches!(self.kind, ResponseCodeKind::AllowedSecurityFunctions(_))
}
pub fn is_decoupled(&self) -> bool {
matches!(self.kind, ResponseCodeKind::DecoupledInitiated)
}
pub fn is_decoupled_pending(&self) -> bool {
matches!(self.kind, ResponseCodeKind::DecoupledPending)
}
pub fn is_pin_wrong(&self) -> bool {
matches!(self.kind, ResponseCodeKind::PinWrong)
}
pub fn is_general_error(&self) -> bool {
matches!(self.kind, ResponseCodeKind::GeneralError)
}
pub fn is_locked(&self) -> bool {
matches!(self.kind, ResponseCodeKind::AccountLocked)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TanMethod {
pub security_function: SecurityFunction,
pub tan_process: TanProcess,
pub name: String,
pub needs_tan_medium: bool,
pub decoupled_max_polls: i32,
pub wait_before_first_poll: i32,
pub wait_before_next_poll: i32,
pub is_decoupled: bool,
pub hktan_version: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SepaAccount {
pub iban: Iban,
pub bic: Bic,
pub account_number: String,
pub sub_account: String,
pub blz: Blz,
pub owner: Option<String>,
pub product_name: Option<String>,
pub currency: Option<Currency>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountBalance {
pub amount: Decimal,
pub date: NaiveDate,
pub currency: Currency,
pub credit_line: Option<Decimal>,
pub available: Option<Decimal>,
pub pending_amount: Option<Decimal>,
pub pending_date: Option<NaiveDate>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransactionStatus {
Booked,
Pending,
}
newtype_string!( Isin);
newtype_string!( Wkn);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityHolding {
pub isin: Option<Isin>,
pub wkn: Option<Wkn>,
pub name: String,
pub quantity: Decimal,
pub price: Option<Decimal>,
pub price_currency: Option<Currency>,
pub price_date: Option<NaiveDate>,
pub market_value: Option<Decimal>,
pub market_value_currency: Option<Currency>,
pub acquisition_value: Option<Decimal>,
pub profit_loss: Option<Decimal>,
pub exchange: Option<String>,
pub depot_id: Option<String>,
pub raw: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
pub date: NaiveDate,
pub valuta_date: Option<NaiveDate>,
pub amount: Decimal,
pub currency: Currency,
pub applicant_name: Option<String>,
pub applicant_iban: Option<Iban>,
pub applicant_bic: Option<Bic>,
pub purpose: Option<String>,
pub posting_text: Option<String>,
pub reference: Option<String>,
pub raw: serde_json::Value,
pub status: TransactionStatus,
}