aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrtkay123 <dev@kanjala.com>2025-08-09 10:36:07 +0200
committerrtkay123 <dev@kanjala.com>2025-08-09 10:36:07 +0200
commitaffa986bf1f84b725bd23309986250ff04cf2c93 (patch)
tree00faafcbdf1962793793e7581984078ce3466085
parent0f663ccb94581264e839bab9ae386114e8bd9973 (diff)
downloadwarden-affa986bf1f84b725bd23309986250ff04cf2c93.tar.bz2
warden-affa986bf1f84b725bd23309986250ff04cf2c93.zip
feat: data cache
-rw-r--r--Cargo.lock55
-rw-r--r--Cargo.toml1
-rw-r--r--crates/warden/Cargo.toml5
-rw-r--r--crates/warden/src/server/routes/processor/pacs008.rs113
-rw-r--r--lib/warden-core/Cargo.toml13
-rw-r--r--lib/warden-core/build.rs13
-rw-r--r--lib/warden-core/src/google.rs2
-rw-r--r--lib/warden-core/src/google/parser.rs4
-rw-r--r--lib/warden-core/src/google/parser/dt.rs233
-rw-r--r--lib/warden-core/src/google/parser/money.rs293
-rw-r--r--lib/warden-core/src/lib.rs5
-rw-r--r--lib/warden-core/src/message.rs1
-rw-r--r--proto/warden_message.proto27
13 files changed, 759 insertions, 6 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 64f80e2..853a015 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -385,6 +385,16 @@ dependencies = [
]
[[package]]
+name = "deranged"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
name = "derive_arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -944,6 +954,12 @@ dependencies = [
]
[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1040,6 +1056,12 @@ dependencies = [
]
[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
name = "prettyplease"
version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1481,6 +1503,37 @@ dependencies = [
]
[[package]]
+name = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
+[[package]]
+name = "time-macros"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
name = "tinystr"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1922,6 +1975,7 @@ dependencies = [
"serde",
"serde_json",
"stack-up",
+ "time",
"tokio",
"tower",
"tracing",
@@ -1941,6 +1995,7 @@ dependencies = [
"prost",
"serde",
"serde_json",
+ "time",
"tonic",
"tonic-prost-build",
"tonic-types",
diff --git a/Cargo.toml b/Cargo.toml
index 0a14a90..5a4c570 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ prost = "0.14.1"
serde = "1.0.219"
serde_json = "1.0.142"
stack-up = { git = "https://github.com/rtkay123/stack-up" }
+time = "0.3.41"
tokio = "1.47.1"
tonic = "0.14.0"
tower = "0.5.2"
diff --git a/crates/warden/Cargo.toml b/crates/warden/Cargo.toml
index 20c646b..231ccae 100644
--- a/crates/warden/Cargo.toml
+++ b/crates/warden/Cargo.toml
@@ -14,6 +14,7 @@ clap = { workspace = true, features = ["derive"] }
config = { workspace = true, features = ["convert-case", "toml"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
+time.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] }
tracing.workspace = true
utoipa = { workspace = true, features = ["axum_extras"] }
@@ -22,10 +23,10 @@ utoipa-rapidoc = { workspace = true, optional = true }
utoipa-redoc = { workspace = true, optional = true }
utoipa-scalar = { workspace = true, optional = true }
utoipa-swagger-ui = { workspace = true, optional = true }
-warden-core = { workspace = true, features = ["iso20022", "openapi"] }
+warden-core = { workspace = true, features = ["iso20022", "serde", "openapi"] }
[features]
-# default = []
+default = []
swagger = ["dep:utoipa-swagger-ui", "utoipa-swagger-ui/axum"]
redoc = ["dep:utoipa-redoc", "utoipa-redoc/axum"]
rapidoc = ["dep:utoipa-rapidoc", "utoipa-rapidoc/axum"]
diff --git a/crates/warden/src/server/routes/processor/pacs008.rs b/crates/warden/src/server/routes/processor/pacs008.rs
index cde5c07..66df598 100644
--- a/crates/warden/src/server/routes/processor/pacs008.rs
+++ b/crates/warden/src/server/routes/processor/pacs008.rs
@@ -1,5 +1,9 @@
use axum::{extract::State, response::IntoResponse};
-use warden_core::iso20022::pacs008::Pacs008Document;
+use tracing::trace;
+use warden_core::{
+ iso20022::{TransactionType, pacs008::Pacs008Document},
+ message::DataCache,
+};
use crate::{error::AppError, server::routes::PACS008_001_12, state::AppHandle, version::Version};
@@ -31,5 +35,112 @@ pub(super) async fn post_pacs008(
State(state): State<AppHandle>,
axum::Json(transaction): axum::Json<Pacs008Document>,
) -> Result<impl IntoResponse, AppError> {
+ let tx_tp = TransactionType::PACS008;
Ok(String::default())
}
+
+pub fn build_data_cache(transaction: &Pacs008Document) -> anyhow::Result<DataCache> {
+ trace!("building data cache object");
+ let cdt_trf_tx_inf = transaction.f_i_to_f_i_cstmr_cdt_trf.cdt_trf_tx_inf.first();
+
+ let instd_amt = cdt_trf_tx_inf.and_then(|value| value.instd_amt.clone());
+
+ let intr_bk_sttlm_amt = cdt_trf_tx_inf.and_then(|value| value.intr_bk_sttlm_amt.clone());
+
+ let xchg_rate = cdt_trf_tx_inf.and_then(|value| value.xchg_rate);
+ let cre_dt_tm = transaction.f_i_to_f_i_cstmr_cdt_trf.grp_hdr.cre_dt_tm;
+
+ let dbtr_othr = cdt_trf_tx_inf.and_then(|value| {
+ value
+ .dbtr
+ .id
+ .as_ref()
+ .and_then(|value| value.prvt_id.othr.first())
+ });
+
+ let debtor_id = dbtr_othr
+ .and_then(|value| {
+ value
+ .schme_nm
+ .as_ref()
+ .map(|schme_nm| format!("{}{}", value.id, schme_nm.prtry))
+ })
+ .ok_or_else(|| anyhow::anyhow!("missing debtor id"))?;
+
+ let cdtr_othr = cdt_trf_tx_inf.and_then(|value| {
+ value.cdtr.as_ref().and_then(|value| {
+ value
+ .id
+ .as_ref()
+ .and_then(|value| value.prvt_id.othr.first())
+ })
+ });
+
+ let creditor_id = cdtr_othr
+ .and_then(|value| {
+ value
+ .schme_nm
+ .as_ref()
+ .map(|schme_nm| format!("{}{}", value.id, schme_nm.prtry))
+ })
+ .ok_or_else(|| anyhow::anyhow!("missing creditor id"))?;
+
+ let dbtr_acct_othr = cdt_trf_tx_inf.and_then(|value| {
+ value
+ .dbtr_acct
+ .as_ref()
+ .and_then(|value| value.id.as_ref().map(|value| value.othr.clone()))
+ });
+ let dbtr_mmb_id = cdt_trf_tx_inf.and_then(|value| {
+ value.dbtr_agt.as_ref().and_then(|value| {
+ value
+ .fin_instn_id
+ .clr_sys_mmb_id
+ .as_ref()
+ .map(|value| value.mmb_id.as_str())
+ })
+ });
+
+ let debtor_acct_id = if let (Some(a), Some(b)) = (dbtr_acct_othr, dbtr_mmb_id) {
+ Some(format!("{}{b}", a.id))
+ } else {
+ None
+ }
+ .ok_or_else(|| anyhow::anyhow!("missing debtor_acct_id"))?;
+
+ let cdtr_acct_othr = cdt_trf_tx_inf.and_then(|value| {
+ value
+ .cdtr_acct
+ .as_ref()
+ .and_then(|value| value.id.as_ref().map(|value| value.othr.clone()))
+ });
+ let cdtr_mmb_id = cdt_trf_tx_inf.and_then(|value| {
+ value.cdtr_agt.as_ref().and_then(|value| {
+ value
+ .fin_instn_id
+ .clr_sys_mmb_id
+ .as_ref()
+ .map(|value| value.mmb_id.as_str())
+ })
+ });
+
+ let creditor_acct_id = if let (Some(a), Some(b)) = (cdtr_acct_othr, cdtr_mmb_id) {
+ Some(format!("{}{b}", a.id))
+ } else {
+ None
+ }
+ .ok_or_else(|| anyhow::anyhow!("missing creditor_acct_id"))?;
+
+ let data_cache = DataCache {
+ cdtr_id: creditor_id.to_string(),
+ dbtr_id: debtor_id.to_string(),
+ dbtr_acct_id: debtor_acct_id.to_string(),
+ cdtr_acct_id: creditor_acct_id.to_string(),
+ cre_dt_tm: Some(cre_dt_tm),
+ instd_amt,
+ intr_bk_sttlm_amt,
+ xchg_rate,
+ };
+
+ Ok(data_cache)
+}
diff --git a/lib/warden-core/Cargo.toml b/lib/warden-core/Cargo.toml
index 9cd4617..bdf0af0 100644
--- a/lib/warden-core/Cargo.toml
+++ b/lib/warden-core/Cargo.toml
@@ -15,6 +15,7 @@ rustdoc-args = ["--cfg", "docsrs"]
prost = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
+time = { workspace = true, optional = true }
tonic = { workspace = true, optional = true }
tonic-types = { version = "0.14.0", optional = true }
utoipa = { workspace = true, optional = true }
@@ -23,7 +24,17 @@ utoipa = { workspace = true, optional = true }
default = []
iso20022 = ["dep:prost", "dep:tonic", "dep:tonic-types"]
serde = ["dep:serde", "serde/derive", "dep:serde_json"]
-openapi = ["dep:utoipa", "serde"]
+serde-time = [
+ "time",
+ "serde",
+ "time/serde",
+]
+time = [
+ "time/parsing",
+ "time/formatting",
+ "time/macros",
+]
+openapi = ["dep:utoipa", "serde-time", "utoipa/time"]
[build-dependencies]
tonic-prost-build = { version = "0.14.0", features = ["cleanup-markdown"] }
diff --git a/lib/warden-core/build.rs b/lib/warden-core/build.rs
index 7ae6b6a..1998137 100644
--- a/lib/warden-core/build.rs
+++ b/lib/warden-core/build.rs
@@ -14,6 +14,7 @@ impl Entity {
vec![
"proto/iso20022/pacs_008_001_12.proto",
"proto/iso20022/pacs_002_001_12.proto",
+ "proto/warden_message.proto",
]
}
@@ -78,10 +79,18 @@ fn build_proto(package: &str, entity: Entity) -> Result<(), Box<dyn std::error::
#[cfg(all(feature = "serde", feature = "iso20022"))]
fn add_serde(config: tonic_prost_build::Builder) -> tonic_prost_build::Builder {
- config.type_attribute(
+ let config = config.type_attribute(
".",
"#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"snake_case\")]",
- )
+ );
+
+ #[cfg(feature = "time")]
+ let config = config.type_attribute(
+ ".google.protobuf.Timestamp",
+ "#[serde(try_from = \"time::OffsetDateTime\")] #[serde(into = \"String\")]",
+ );
+
+ config
}
#[cfg(all(feature = "openapi", feature = "iso20022"))]
diff --git a/lib/warden-core/src/google.rs b/lib/warden-core/src/google.rs
index 365766c..0e9487d 100644
--- a/lib/warden-core/src/google.rs
+++ b/lib/warden-core/src/google.rs
@@ -1,3 +1,5 @@
+mod parser;
+
/// Well known types
pub mod protobuf {
include!(concat!(env!("OUT_DIR"), "/google.protobuf.rs"));
diff --git a/lib/warden-core/src/google/parser.rs b/lib/warden-core/src/google/parser.rs
new file mode 100644
index 0000000..7405077
--- /dev/null
+++ b/lib/warden-core/src/google/parser.rs
@@ -0,0 +1,4 @@
+#[cfg(feature = "time")]
+mod dt;
+
+//mod money;
diff --git a/lib/warden-core/src/google/parser/dt.rs b/lib/warden-core/src/google/parser/dt.rs
new file mode 100644
index 0000000..ced6f12
--- /dev/null
+++ b/lib/warden-core/src/google/parser/dt.rs
@@ -0,0 +1,233 @@
+use crate::google::{protobuf::Timestamp, r#type::Date};
+
+impl From<time::OffsetDateTime> for Date {
+ fn from(dt: time::OffsetDateTime) -> Self {
+ Self {
+ year: dt.year(),
+ month: dt.month() as i32,
+ day: dt.day() as i32,
+ }
+ }
+}
+
+impl From<time::Date> for Date {
+ fn from(value: time::Date) -> Self {
+ Self {
+ year: value.year(),
+ month: value.month() as i32,
+ day: value.day() as i32,
+ }
+ }
+}
+
+impl TryFrom<Date> for time::Date {
+ type Error = time::Error;
+
+ fn try_from(value: Date) -> Result<Self, Self::Error> {
+ Ok(Self::from_calendar_date(
+ value.year,
+ time::Month::try_from(value.month as u8)?,
+ value.day as u8,
+ )?)
+ }
+}
+
+impl std::str::FromStr for Date {
+ type Err = time::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let date = time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
+ .map(Date::from);
+
+ match date {
+ Ok(dt) => Ok(dt),
+ Err(_e) => {
+ let my_format = time::macros::format_description!("[year]-[month]-[day]");
+ let date = time::Date::parse(s, &my_format)?;
+ Ok(Date::from(date))
+ }
+ }
+ }
+}
+
+impl TryFrom<String> for Date {
+ type Error = time::Error;
+
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ <Date as std::str::FromStr>::from_str(&value)
+ }
+}
+
+impl TryFrom<DateItem> for Date {
+ type Error = time::Error;
+
+ fn try_from(value: DateItem) -> Result<Self, Self::Error> {
+ match value {
+ DateItem::String(ref string) => <Date as std::str::FromStr>::from_str(string),
+ #[cfg(feature = "iso20022")]
+ DateItem::Date { year, month, day } => Ok(Date { year, month, day }),
+ DateItem::Timestamp { seconds, nanos } => {
+ let odt = time::OffsetDateTime::try_from(crate::google::protobuf::Timestamp {
+ seconds,
+ nanos,
+ })?;
+ Ok(Self {
+ year: odt.year(),
+ month: odt.month() as i32,
+ day: odt.day() as i32,
+ })
+ }
+ }
+ }
+}
+
+impl From<Date> for String {
+ fn from(value: Date) -> Self {
+ let prepend = |value: i32| -> String {
+ match value.lt(&10) {
+ true => format!("0{}", value),
+ false => value.to_string(),
+ }
+ };
+ format!(
+ "{}-{}-{}",
+ value.year,
+ prepend(value.month),
+ prepend(value.day),
+ )
+ }
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(untagged))]
+/// Date utility
+#[derive(Clone, Debug)]
+pub enum DateItem {
+ /// string
+ String(String),
+ /// ts
+ Timestamp { seconds: i64, nanos: i32 },
+ /// date
+ #[cfg(feature = "iso20022")]
+ Date { year: i32, month: i32, day: i32 },
+}
+
+impl TryFrom<DateItem> for Timestamp {
+ type Error = time::Error;
+
+ fn try_from(value: DateItem) -> Result<Self, Self::Error> {
+ match value {
+ DateItem::String(ref string) => <Timestamp as std::str::FromStr>::from_str(string),
+ #[cfg(feature = "iso20022")]
+ DateItem::Date { year, month, day } => {
+ let date = time::Date::try_from(crate::google::r#type::Date { year, month, day })?;
+ let time = time::Time::MIDNIGHT;
+ let offset = time::UtcOffset::UTC;
+ Ok(date.with_time(time).assume_offset(offset).into())
+ }
+ DateItem::Timestamp { seconds, nanos } => Ok(Self { seconds, nanos }),
+ }
+ }
+}
+
+impl From<time::OffsetDateTime> for Timestamp {
+ fn from(dt: time::OffsetDateTime) -> Self {
+ Timestamp {
+ seconds: dt.unix_timestamp(),
+ nanos: dt.nanosecond() as i32,
+ }
+ }
+}
+
+impl From<Timestamp> for String {
+ fn from(value: Timestamp) -> Self {
+ let odt = time::OffsetDateTime::try_from(value).expect("invalid date");
+ odt.format(&time::format_description::well_known::Rfc3339)
+ .expect("format is not rfc3339")
+ }
+}
+
+impl std::str::FromStr for Timestamp {
+ type Err = time::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let timestamp =
+ time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)?;
+
+ Ok(Timestamp::from(timestamp))
+ }
+}
+
+impl TryFrom<String> for Timestamp {
+ type Error = time::Error;
+
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ <Timestamp as std::str::FromStr>::from_str(&value)
+ }
+}
+
+impl TryFrom<Timestamp> for time::OffsetDateTime {
+ type Error = time::Error;
+
+ fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
+ let dt = time::OffsetDateTime::from_unix_timestamp(value.seconds)?;
+
+ Ok(dt.replace_nanosecond(value.nanos as u32)?)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use time::{Duration, OffsetDateTime};
+
+ #[test]
+ fn test_offsetdatetime_to_timestamp() {
+ let now = OffsetDateTime::now_utc();
+ let timestamp: Timestamp = now.into();
+
+ assert_eq!(timestamp.seconds, now.unix_timestamp());
+ assert_eq!(timestamp.nanos, now.nanosecond() as i32);
+ }
+
+ #[test]
+ fn test_timestamp_to_offsetdatetime() {
+ let now = OffsetDateTime::now_utc();
+ let timestamp: Timestamp = now.into();
+ let dt: OffsetDateTime = timestamp.try_into().unwrap();
+
+ assert_eq!(dt, now);
+ }
+
+ #[test]
+ fn test_timestamp_to_offsetdatetime_with_nanos() {
+ let now = OffsetDateTime::now_utc();
+ let nanos = 123456789;
+ let dt = now + Duration::nanoseconds(nanos);
+ let timestamp: Timestamp = dt.into();
+ let dt_from_timestamp: OffsetDateTime = timestamp.try_into().unwrap();
+
+ assert_eq!(dt_from_timestamp, dt);
+ }
+
+ #[test]
+ fn test_timestamp_to_offsetdatetime_with_negative_nanos() {
+ let now = OffsetDateTime::now_utc();
+ let nanos = -123456789;
+ let dt = now + Duration::nanoseconds(nanos);
+ let timestamp: Timestamp = dt.into();
+ let dt_from_timestamp: OffsetDateTime = timestamp.try_into().unwrap();
+
+ assert_eq!(dt_from_timestamp, dt);
+ }
+
+ #[test]
+ fn test_timestamp_to_offsetdatetime_invalid_seconds() {
+ let timestamp = Timestamp {
+ seconds: i64::MIN,
+ nanos: 0,
+ };
+ let result: Result<OffsetDateTime, time::Error> = timestamp.try_into();
+ assert!(result.is_err());
+ }
+}
diff --git a/lib/warden-core/src/google/parser/money.rs b/lib/warden-core/src/google/parser/money.rs
new file mode 100644
index 0000000..54ed5fa
--- /dev/null
+++ b/lib/warden-core/src/google/parser/money.rs
@@ -0,0 +1,293 @@
+use crate::google::r#type::Date;
+
+/// If money cannot be created
+#[derive(Debug, PartialEq)]
+pub enum MoneyError {
+ /// Invalid currency code
+ InvalidCurrencyCode,
+}
+
+impl<T> TryFrom<(f64, T)> for Money
+where
+ T: AsRef<str>,
+{
+ type Error = MoneyError;
+
+ fn try_from((value, ccy): (f64, T)) -> Result<Self, Self::Error> {
+ let currency_code = ccy.as_ref();
+
+ let is_valid =
+ currency_code.len() == 3 && currency_code.chars().all(|c| c.is_ascii_uppercase());
+
+ if !is_valid {
+ return Err(MoneyError::InvalidCurrencyCode);
+ }
+
+ let units = value.trunc() as i64;
+ let nanos_raw = ((value - units as f64) * 1_000_000_000.0).round() as i32;
+
+ let (units, nanos) = if units > 0 && nanos_raw < 0 {
+ (units - 1, nanos_raw + 1_000_000_000)
+ } else if units < 0 && nanos_raw > 0 {
+ (units + 1, nanos_raw - 1_000_000_000)
+ } else {
+ (units, nanos_raw)
+ };
+
+ Ok(Money {
+ currency_code: currency_code.to_string(),
+ units,
+ nanos,
+ })
+ }
+}
+
+impl From<Money> for f64 {
+ fn from(m: Money) -> Self {
+ m.units as f64 + (m.nanos as f64) / 1_000_000_000.0
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::convert::TryFrom;
+
+ #[test]
+ fn test_valid_money_conversion() {
+ let money = Money::try_from((1.75, "USD")).unwrap();
+ assert_eq!(
+ money,
+ Money {
+ currency_code: "USD".to_string(),
+ units: 1,
+ nanos: 750_000_000
+ }
+ );
+
+ let money = Money::try_from((-1.75, "EUR")).unwrap();
+ assert_eq!(
+ money,
+ Money {
+ currency_code: "EUR".to_string(),
+ units: -1,
+ nanos: -750_000_000
+ }
+ );
+
+ let money = Money::try_from((0.333_333_333, "JPY")).unwrap();
+ assert_eq!(
+ money,
+ Money {
+ currency_code: "JPY".to_string(),
+ units: 0,
+ nanos: 333_333_333
+ }
+ );
+ }
+
+ #[test]
+ fn test_sign_correction() {
+ let m = Money::try_from((1.000_000_001, "USD")).unwrap();
+ assert_eq!(m.units, 1);
+ assert_eq!(m.nanos, 1);
+
+ let m = Money::try_from((-1.000_000_001, "USD")).unwrap();
+ assert_eq!(m.units, -1);
+ assert_eq!(m.nanos, -1);
+
+ let m = Money::try_from((0.000_000_001, "USD")).unwrap();
+ assert_eq!(m.units, 0);
+ assert_eq!(m.nanos, 1);
+
+ let m = Money::try_from((-0.000_000_001, "USD")).unwrap();
+ assert_eq!(m.units, 0);
+ assert_eq!(m.nanos, -1);
+ }
+
+ #[test]
+ fn test_invalid_currency_code() {
+ let cases = ["usd", "US", "USDD", "12A", "€€€", ""];
+
+ for &code in &cases {
+ let result = Money::try_from((1.0, code));
+ assert_eq!(result, Err(MoneyError::InvalidCurrencyCode));
+ }
+ }
+
+ #[test]
+ fn test_money_to_f64_positive() {
+ let money = Money {
+ currency_code: "USD".to_string(),
+ units: 1,
+ nanos: 750_000_000,
+ };
+ let value: f64 = money.into();
+ assert_eq!(value, 1.75);
+ }
+
+ #[test]
+ fn test_money_to_f64_negative() {
+ let money = Money {
+ currency_code: "EUR".to_string(),
+ units: -1,
+ nanos: -250_000_000,
+ };
+ let value: f64 = money.into();
+ assert_eq!(value, -1.25);
+ }
+
+ #[test]
+ fn test_money_to_f64_zero() {
+ let money = Money {
+ currency_code: "JPY".to_string(),
+ units: 0,
+ nanos: 0,
+ };
+ let value: f64 = money.into();
+ assert_eq!(value, 0.0);
+ }
+
+ #[test]
+ fn test_money_to_f64_fractional_positive() {
+ let money = Money {
+ currency_code: "USD".to_string(),
+ units: 0,
+ nanos: 1,
+ };
+ let value: f64 = money.into();
+ assert_eq!(value, 1.0e-9);
+ }
+
+ #[test]
+ fn test_money_to_f64_fractional_negative() {
+ let money = Money {
+ currency_code: "USD".to_string(),
+ units: 0,
+ nanos: -1,
+ };
+ let value: f64 = money.into();
+ assert_eq!(value, -1.0e-9);
+ }
+
+ #[test]
+ fn test_round_trip_conversion() {
+ let original = 1234.567_890_123;
+ let money = Money::try_from((original, "USD")).unwrap();
+ let back: f64 = money.into();
+ assert!(
+ (original - back).abs() < 1e-9,
+ "got {}, expected {}",
+ back,
+ original
+ );
+ }
+}
+
+#[cfg(feature = "time")]
+impl From<time::OffsetDateTime> for Date {
+ fn from(dt: time::OffsetDateTime) -> Self {
+ Self {
+ year: dt.year(),
+ month: dt.month() as i32,
+ day: dt.day() as i32,
+ }
+ }
+}
+
+#[cfg(feature = "time")]
+impl From<time::Date> for Date {
+ fn from(value: time::Date) -> Self {
+ Self {
+ year: value.year(),
+ month: value.month() as i32,
+ day: value.day() as i32,
+ }
+ }
+}
+
+#[cfg(feature = "time")]
+impl TryFrom<Date> for time::Date {
+ type Error = time::Error;
+
+ fn try_from(value: Date) -> Result<Self, Self::Error> {
+ Ok(Self::from_calendar_date(
+ value.year,
+ time::Month::try_from(value.month as u8)?,
+ value.day as u8,
+ )?)
+ }
+}
+
+#[cfg(feature = "time")]
+impl std::str::FromStr for Date {
+ type Err = time::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let date = time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
+ .map(Date::from);
+
+ match date {
+ Ok(dt) => Ok(dt),
+ Err(_e) => {
+ let my_format = time::macros::format_description!("[year]-[month]-[day]");
+ let date = time::Date::parse(&s, &my_format)?;
+ Ok(Date::from(date))
+ }
+ }
+ }
+}
+
+#[cfg(feature = "time")]
+impl TryFrom<String> for Date {
+ type Error = time::Error;
+
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ <Date as std::str::FromStr>::from_str(&value)
+ }
+}
+
+#[cfg(feature = "time")]
+impl TryFrom<super::helpers::time_util::DateItem> for Date {
+ type Error = time::Error;
+
+ fn try_from(value: super::helpers::time_util::DateItem) -> Result<Self, Self::Error> {
+ match value {
+ super::helpers::time_util::DateItem::String(ref string) => {
+ <Date as std::str::FromStr>::from_str(string)
+ }
+ #[cfg(feature = "iso20022")]
+ super::helpers::time_util::DateItem::Date { year, month, day } => {
+ Ok(Date { year, month, day })
+ }
+ super::helpers::time_util::DateItem::Timestamp { seconds, nanos } => {
+ let odt = time::OffsetDateTime::try_from(crate::google::protobuf::Timestamp {
+ seconds,
+ nanos,
+ })?;
+ Ok(Self {
+ year: odt.year(),
+ month: odt.month() as i32,
+ day: odt.day() as i32,
+ })
+ }
+ }
+ }
+}
+
+impl From<Date> for String {
+ fn from(value: Date) -> Self {
+ let prepend = |value: i32| -> String {
+ match value.lt(&10) {
+ true => format!("0{}", value),
+ false => value.to_string(),
+ }
+ };
+ format!(
+ "{}-{}-{}",
+ value.year,
+ prepend(value.month),
+ prepend(value.day),
+ )
+ }
+}
diff --git a/lib/warden-core/src/lib.rs b/lib/warden-core/src/lib.rs
index 111ce5d..19d02f3 100644
--- a/lib/warden-core/src/lib.rs
+++ b/lib/warden-core/src/lib.rs
@@ -15,3 +15,8 @@ pub mod google;
#[allow(missing_docs)]
#[cfg(feature = "iso20022")]
pub mod iso20022;
+
+/// Message in transit
+#[allow(missing_docs)]
+#[cfg(feature = "iso20022")]
+pub mod message;
diff --git a/lib/warden-core/src/message.rs b/lib/warden-core/src/message.rs
new file mode 100644
index 0000000..4b8142b
--- /dev/null
+++ b/lib/warden-core/src/message.rs
@@ -0,0 +1 @@
+tonic::include_proto!("message");
diff --git a/proto/warden_message.proto b/proto/warden_message.proto
new file mode 100644
index 0000000..d9c0cd4
--- /dev/null
+++ b/proto/warden_message.proto
@@ -0,0 +1,27 @@
+syntax = "proto3";
+
+package message;
+
+import "proto/iso20022/pacs_008_001_12.proto";
+import "proto/iso20022/pacs_002_001_12.proto";
+import "google/protobuf/timestamp.proto";
+
+message Payload {
+ oneof transaction {
+ iso20022.pacs008.Pacs008Document pacs008 = 1;
+ iso20022.pacs002.Pacs002Document pacs002 = 2;
+ }
+ DataCache data_cache = 3;
+ string tx_tp = 4;
+}
+
+message DataCache {
+ string cdtr_id = 1;
+ string dbtr_id = 2;
+ string dbtr_acct_id = 3;
+ string cdtr_acct_id = 4;
+ google.protobuf.Timestamp cre_dt_tm = 5;
+ iso20022.pacs008.ActiveOrHistoricCurrencyAndAmount instd_amt = 6;
+ iso20022.pacs008.ActiveCurrencyAndAmount intr_bk_sttlm_amt = 7;
+ optional double xchg_rate = 8;
+}