1use serde::{Deserialize, Serialize};
4
5use crate::error::MxError;
7pub use crate::header::AppHdr;
8use crate::message_registry;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub enum Document {
15 #[serde(rename = "FIToFICstmrCdtTrf")]
17 Pacs008(Box<crate::document::pacs_008_001_08::FIToFICustomerCreditTransferV08>),
18
19 #[serde(rename = "FIToFIPmtStsRpt")]
20 Pacs002(Box<crate::document::pacs_002_001_10::FIToFIPaymentStatusReportV10>),
21
22 #[serde(rename = "FIToFICstmrDrctDbt")]
23 Pacs003(Box<crate::document::pacs_003_001_08::FIToFICustomerDirectDebitV08>),
24
25 #[serde(rename = "PmtRtr")]
26 Pacs004(Box<crate::document::pacs_004_001_09::PaymentReturnV09>),
27
28 #[serde(rename = "FICdtTrf")]
29 Pacs009(Box<crate::document::pacs_009_001_08::FinancialInstitutionCreditTransferV08>),
30
31 #[serde(rename = "FIDrctDbt")]
32 Pacs010(Box<crate::document::pacs_010_001_03::FinancialInstitutionDirectDebitV03>),
33
34 #[serde(rename = "CstmrCdtTrfInitn")]
36 Pain001(Box<crate::document::pain_001_001_09::CustomerCreditTransferInitiationV09>),
37
38 #[serde(rename = "CstmrPmtStsRpt")]
39 Pain002(Box<crate::document::pain_002_001_10::CustomerPaymentStatusReportV10>),
40
41 #[serde(rename = "CstmrDrctDbtInitn")]
42 Pain008(Box<crate::document::pain_008_001_08::CustomerDirectDebitInitiationV08>),
43
44 #[serde(rename = "Rcpt")]
46 Camt025(Box<crate::document::camt_025_001_08::ReceiptV08>),
47
48 #[serde(rename = "RsltnOfInvstgtn")]
49 Camt029(Box<crate::document::camt_029_001_09::ResolutionOfInvestigationV09>),
50
51 #[serde(rename = "BkToCstmrAcctRpt")]
52 Camt052(Box<crate::document::camt_052_001_08::BankToCustomerAccountReportV08>),
53
54 #[serde(rename = "BkToCstmrStmt")]
55 Camt053(Box<crate::document::camt_053_001_08::BankToCustomerStatementV08>),
56
57 #[serde(rename = "BkToCstmrDbtCdtNtfctn")]
58 Camt054(Box<crate::document::camt_054_001_08::BankToCustomerDebitCreditNotificationV08>),
59
60 #[serde(rename = "FIToFIPmtCxlReq")]
61 Camt056(Box<crate::document::camt_056_001_08::FIToFIPaymentCancellationRequestV08>),
62
63 #[serde(rename = "NtfctnToRcv")]
64 Camt057(Box<crate::document::camt_057_001_06::NotificationToReceiveV06>),
65
66 #[serde(rename = "AcctRptgReq")]
67 Camt060(Box<crate::document::camt_060_001_05::AccountReportingRequestV05>),
68
69 #[serde(rename = "ChqPresntmntNtfctn")]
70 Camt107(Box<crate::document::camt_107_001_01::ChequePresentmentNotificationV01>),
71
72 #[serde(rename = "ChqCxlOrStopReq")]
73 Camt108(Box<crate::document::camt_108_001_01::ChequeCancellationOrStopRequestV01>),
74
75 #[serde(rename = "ChqCxlOrStopRpt")]
76 Camt109(Box<crate::document::camt_109_001_01::ChequeCancellationOrStopReportV01>),
77
78 #[serde(rename = "NtfctnOfCrrspndnc")]
80 Admi024(Box<crate::document::admi_024_001_01::NotificationOfCorrespondenceV01>),
81}
82
83impl Document {
84 pub fn namespace(&self) -> String {
86 let msg_type = match self {
87 Document::Pacs008(_) => "pacs.008",
88 Document::Pacs009(_) => "pacs.009",
89 Document::Pacs003(_) => "pacs.003",
90 Document::Pacs004(_) => "pacs.004",
91 Document::Pacs002(_) => "pacs.002",
92 Document::Pacs010(_) => "pacs.010",
93 Document::Pain001(_) => "pain.001",
94 Document::Pain002(_) => "pain.002",
95 Document::Pain008(_) => "pain.008",
96 Document::Camt025(_) => "camt.025",
97 Document::Camt029(_) => "camt.029",
98 Document::Camt052(_) => "camt.052",
99 Document::Camt053(_) => "camt.053",
100 Document::Camt054(_) => "camt.054",
101 Document::Camt056(_) => "camt.056",
102 Document::Camt057(_) => "camt.057",
103 Document::Camt060(_) => "camt.060",
104 Document::Camt107(_) => "camt.107",
105 Document::Camt108(_) => "camt.108",
106 Document::Camt109(_) => "camt.109",
107 Document::Admi024(_) => "admi.024",
108 };
109 message_registry::get_namespace(msg_type)
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117#[serde(rename = "Envelope")]
118pub struct MxMessage {
119 #[serde(rename = "@xmlns", skip_serializing_if = "Option::is_none")]
121 pub xmlns: Option<String>,
122
123 #[serde(rename = "@xmlns:xsi", skip_serializing_if = "Option::is_none")]
124 pub xmlns_xsi: Option<String>,
125
126 #[serde(rename = "AppHdr")]
128 pub app_hdr: crate::header::AppHdr,
129
130 #[serde(rename = "Document")]
132 pub document: Document,
133}
134
135impl MxMessage {
136 pub fn new(app_hdr: crate::header::AppHdr, document: Document) -> Self {
138 Self {
139 xmlns: Some("urn:iso:std:iso:20022:tech:xsd:head.001.001.02".to_string()),
140 xmlns_xsi: Some("http://www.w3.org/2001/XMLSchema-instance".to_string()),
141 app_hdr,
142 document,
143 }
144 }
145}
146
147pub fn get_namespace_for_message_type(message_type: &str) -> String {
150 message_registry::get_namespace(message_type)
151}
152
153pub fn normalize_message_type(message_type: &str) -> String {
156 message_registry::normalize_message_type(message_type)
157}
158
159macro_rules! serialize_doc {
161 ($doc:expr, $rust_type:expr, $xml_elem:expr, $msg_type:expr) => {
162 MxMessage::serialize_with_rename($doc.as_ref(), $rust_type, $xml_elem, $msg_type)
163 };
164}
165
166macro_rules! deserialize_doc {
168 ($xml:expr, $path:path, $variant:ident, $msg_type:expr) => {{
169 let doc = quick_xml::de::from_str::<$path>($xml).map_err(|e| {
170 MxError::XmlDeserialization(format!("Failed to parse {}: {}", $msg_type, e))
171 })?;
172 Ok(Document::$variant(Box::new(doc)))
173 }};
174}
175
176impl MxMessage {
177 pub fn message_type(&self) -> Result<&str, MxError> {
179 Ok(&self.app_hdr.msg_def_idr)
180 }
181
182 pub fn namespace(&self) -> Result<String, MxError> {
184 Ok(get_namespace_for_message_type(self.message_type()?))
185 }
186
187 fn serialize_with_rename<T: Serialize>(
189 value: &T,
190 rust_type: &str,
191 xml_element: &str,
192 msg_type: &str,
193 ) -> Result<String, MxError> {
194 let xml = quick_xml::se::to_string(value).map_err(|e| {
195 MxError::XmlSerialization(format!("Failed to serialize {}: {}", msg_type, e))
196 })?;
197 Ok(xml
198 .replace(&format!("<{}>", rust_type), &format!("<{}>", xml_element))
199 .replace(&format!("</{}>", rust_type), &format!("</{}>", xml_element)))
200 }
201
202 pub fn to_xml(&self) -> Result<String, MxError> {
204 let app_hdr_xml = quick_xml::se::to_string(&self.app_hdr)
207 .map_err(|e| MxError::XmlSerialization(format!("Failed to serialize AppHdr: {}", e)))?;
208
209 let app_hdr_inner = app_hdr_xml;
211
212 let doc_xml = self.serialize_document()?;
214
215 let app_hdr_wrapped = app_hdr_inner
219 .replace("<BusinessApplicationHeaderV02>", "<AppHdr>")
220 .replace("</BusinessApplicationHeaderV02>", "</AppHdr>");
221
222 let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
223 xml.push_str("<Envelope>");
224 xml.push_str(&app_hdr_wrapped);
225 xml.push_str("<Document>");
226 xml.push_str(&doc_xml);
227 xml.push_str("</Document>");
228 xml.push_str("</Envelope>");
229
230 Ok(xml)
231 }
232
233 fn serialize_document(&self) -> Result<String, MxError> {
235 match &self.document {
236 Document::Pacs008(doc) => serialize_doc!(
237 doc,
238 "FIToFICustomerCreditTransferV08",
239 "FIToFICstmrCdtTrf",
240 "pacs.008"
241 ),
242 Document::Pacs002(doc) => serialize_doc!(
243 doc,
244 "FIToFIPaymentStatusReportV10",
245 "FIToFIPmtStsRpt",
246 "pacs.002"
247 ),
248 Document::Pacs003(doc) => serialize_doc!(
249 doc,
250 "FIToFICustomerDirectDebitV08",
251 "FIToFICstmrDrctDbt",
252 "pacs.003"
253 ),
254 Document::Pacs004(doc) => serialize_doc!(doc, "PaymentReturnV09", "PmtRtr", "pacs.004"),
255 Document::Pacs009(doc) => serialize_doc!(
256 doc,
257 "FinancialInstitutionCreditTransferV08",
258 "FICdtTrf",
259 "pacs.009"
260 ),
261 Document::Pacs010(doc) => serialize_doc!(
262 doc,
263 "FinancialInstitutionDirectDebitV03",
264 "FIDrctDbt",
265 "pacs.010"
266 ),
267 Document::Pain001(doc) => serialize_doc!(
268 doc,
269 "CustomerCreditTransferInitiationV09",
270 "CstmrCdtTrfInitn",
271 "pain.001"
272 ),
273 Document::Pain002(doc) => serialize_doc!(
274 doc,
275 "CustomerPaymentStatusReportV10",
276 "CstmrPmtStsRpt",
277 "pain.002"
278 ),
279 Document::Pain008(doc) => serialize_doc!(
280 doc,
281 "CustomerDirectDebitInitiationV08",
282 "CstmrDrctDbtInitn",
283 "pain.008"
284 ),
285 Document::Camt025(doc) => serialize_doc!(doc, "ReceiptV08", "Rcpt", "camt.025"),
286 Document::Camt029(doc) => serialize_doc!(
287 doc,
288 "ResolutionOfInvestigationV09",
289 "RsltnOfInvstgtn",
290 "camt.029"
291 ),
292 Document::Camt052(doc) => serialize_doc!(
293 doc,
294 "BankToCustomerAccountReportV08",
295 "BkToCstmrAcctRpt",
296 "camt.052"
297 ),
298 Document::Camt053(doc) => serialize_doc!(
299 doc,
300 "BankToCustomerStatementV08",
301 "BkToCstmrStmt",
302 "camt.053"
303 ),
304 Document::Camt054(doc) => serialize_doc!(
305 doc,
306 "BankToCustomerDebitCreditNotificationV08",
307 "BkToCstmrDbtCdtNtfctn",
308 "camt.054"
309 ),
310 Document::Camt056(doc) => serialize_doc!(
311 doc,
312 "FIToFIPaymentCancellationRequestV08",
313 "FIToFIPmtCxlReq",
314 "camt.056"
315 ),
316 Document::Camt057(doc) => {
317 serialize_doc!(doc, "NotificationToReceiveV06", "NtfctnToRcv", "camt.057")
318 }
319 Document::Camt060(doc) => {
320 serialize_doc!(doc, "AccountReportingRequestV05", "AcctRptgReq", "camt.060")
321 }
322 Document::Camt107(doc) => serialize_doc!(
323 doc,
324 "ChequePresentmentNotificationV01",
325 "ChqPresntmntNtfctn",
326 "camt.107"
327 ),
328 Document::Camt108(doc) => serialize_doc!(
329 doc,
330 "ChequeCancellationOrStopRequestV01",
331 "ChqCxlOrStopReq",
332 "camt.108"
333 ),
334 Document::Camt109(doc) => serialize_doc!(
335 doc,
336 "ChequeCancellationOrStopReportV01",
337 "ChqCxlOrStopRpt",
338 "camt.109"
339 ),
340 Document::Admi024(doc) => serialize_doc!(
341 doc,
342 "NotificationOfCorrespondenceV01",
343 "NtfctnOfCrrspndnc",
344 "admi.024"
345 ),
346 }
347 }
348
349 pub fn to_json(&self) -> Result<String, MxError> {
351 serde_json::to_string_pretty(self).map_err(|e| MxError::XmlSerialization(e.to_string()))
352 }
353
354 pub fn from_xml(xml: &str) -> Result<Self, MxError> {
356 let has_envelope = xml.contains("<AppHdr") || xml.contains("<Envelope");
358
359 if has_envelope {
360 Self::from_xml_with_envelope(xml)
361 } else {
362 Self::from_xml_document_only(xml)
363 }
364 }
365
366 fn from_xml_with_envelope(xml: &str) -> Result<Self, MxError> {
368 let app_hdr_xml = Self::extract_section(xml, "AppHdr")
370 .ok_or_else(|| MxError::XmlDeserialization("AppHdr not found in XML".to_string()))?;
371
372 let app_hdr: crate::header::AppHdr =
374 quick_xml::de::from_str(&format!("<AppHdr>{}</AppHdr>", app_hdr_xml)).map_err(|e| {
375 MxError::XmlDeserialization(format!("Failed to parse AppHdr: {}", e))
376 })?;
377
378 let doc_xml = Self::extract_section(xml, "Document")
380 .ok_or_else(|| MxError::XmlDeserialization("Document not found in XML".to_string()))?;
381
382 let doc_type = Self::detect_document_type(&doc_xml)?;
384
385 let document = Self::deserialize_document(&doc_xml, &doc_type)?;
387
388 let xmlns = Self::extract_attribute(xml, "xmlns");
390 let xmlns_xsi = Self::extract_attribute(xml, "xmlns:xsi");
391
392 Ok(MxMessage {
393 xmlns,
394 xmlns_xsi,
395 app_hdr,
396 document,
397 })
398 }
399
400 fn from_xml_document_only(_xml: &str) -> Result<Self, MxError> {
402 Err(MxError::XmlDeserialization(
405 "Document-only XML requires AppHdr information. Use full envelope format.".to_string(),
406 ))
407 }
408
409 fn extract_section(xml: &str, tag: &str) -> Option<String> {
411 let start_tag = format!("<{}", tag);
412 let end_tag = format!("</{}>", tag);
413
414 let start_idx = xml.find(&start_tag)?;
415 let content_start = xml[start_idx..].find('>')? + start_idx + 1;
416 let end_idx = xml.find(&end_tag)?;
417
418 if content_start < end_idx {
419 Some(xml[content_start..end_idx].to_string())
420 } else {
421 None
422 }
423 }
424
425 fn extract_attribute(xml: &str, attr: &str) -> Option<String> {
427 let pattern = format!("{}=\"", attr);
428 let start_idx = xml.find(&pattern)? + pattern.len();
429 let end_idx = xml[start_idx..].find('"')? + start_idx;
430 Some(xml[start_idx..end_idx].to_string())
431 }
432
433 fn detect_document_type(doc_xml: &str) -> Result<String, MxError> {
435 let trimmed = doc_xml.trim();
437 if !trimmed.starts_with('<') {
438 return Err(MxError::XmlDeserialization(
439 "Invalid document XML structure".to_string(),
440 ));
441 }
442
443 let end_idx = trimmed[1..]
444 .find(|c: char| c.is_whitespace() || c == '>')
445 .map(|i| i + 1)
446 .ok_or_else(|| {
447 MxError::XmlDeserialization("Could not find document element".to_string())
448 })?;
449
450 let element_name = &trimmed[1..end_idx];
451
452 let message_type =
454 message_registry::element_to_message_type(element_name).ok_or_else(|| {
455 MxError::XmlDeserialization(format!("Unknown document type: {}", element_name))
456 })?;
457
458 Ok(message_type.to_string())
459 }
460
461 fn deserialize_document(doc_xml: &str, message_type: &str) -> Result<Document, MxError> {
463 use crate::document::*;
464
465 match message_type {
466 "pacs.008" => deserialize_doc!(
467 doc_xml,
468 pacs_008_001_08::FIToFICustomerCreditTransferV08,
469 Pacs008,
470 "pacs.008"
471 ),
472 "pacs.002" => deserialize_doc!(
473 doc_xml,
474 pacs_002_001_10::FIToFIPaymentStatusReportV10,
475 Pacs002,
476 "pacs.002"
477 ),
478 "pacs.003" => deserialize_doc!(
479 doc_xml,
480 pacs_003_001_08::FIToFICustomerDirectDebitV08,
481 Pacs003,
482 "pacs.003"
483 ),
484 "pacs.004" => deserialize_doc!(
485 doc_xml,
486 pacs_004_001_09::PaymentReturnV09,
487 Pacs004,
488 "pacs.004"
489 ),
490 "pacs.009" => deserialize_doc!(
491 doc_xml,
492 pacs_009_001_08::FinancialInstitutionCreditTransferV08,
493 Pacs009,
494 "pacs.009"
495 ),
496 "pacs.010" => deserialize_doc!(
497 doc_xml,
498 pacs_010_001_03::FinancialInstitutionDirectDebitV03,
499 Pacs010,
500 "pacs.010"
501 ),
502 "pain.001" => deserialize_doc!(
503 doc_xml,
504 pain_001_001_09::CustomerCreditTransferInitiationV09,
505 Pain001,
506 "pain.001"
507 ),
508 "pain.002" => deserialize_doc!(
509 doc_xml,
510 pain_002_001_10::CustomerPaymentStatusReportV10,
511 Pain002,
512 "pain.002"
513 ),
514 "pain.008" => deserialize_doc!(
515 doc_xml,
516 pain_008_001_08::CustomerDirectDebitInitiationV08,
517 Pain008,
518 "pain.008"
519 ),
520 "camt.025" => {
521 deserialize_doc!(doc_xml, camt_025_001_08::ReceiptV08, Camt025, "camt.025")
522 }
523 "camt.029" => deserialize_doc!(
524 doc_xml,
525 camt_029_001_09::ResolutionOfInvestigationV09,
526 Camt029,
527 "camt.029"
528 ),
529 "camt.052" => deserialize_doc!(
530 doc_xml,
531 camt_052_001_08::BankToCustomerAccountReportV08,
532 Camt052,
533 "camt.052"
534 ),
535 "camt.053" => deserialize_doc!(
536 doc_xml,
537 camt_053_001_08::BankToCustomerStatementV08,
538 Camt053,
539 "camt.053"
540 ),
541 "camt.054" => deserialize_doc!(
542 doc_xml,
543 camt_054_001_08::BankToCustomerDebitCreditNotificationV08,
544 Camt054,
545 "camt.054"
546 ),
547 "camt.056" => deserialize_doc!(
548 doc_xml,
549 camt_056_001_08::FIToFIPaymentCancellationRequestV08,
550 Camt056,
551 "camt.056"
552 ),
553 "camt.057" => deserialize_doc!(
554 doc_xml,
555 camt_057_001_06::NotificationToReceiveV06,
556 Camt057,
557 "camt.057"
558 ),
559 "camt.060" => deserialize_doc!(
560 doc_xml,
561 camt_060_001_05::AccountReportingRequestV05,
562 Camt060,
563 "camt.060"
564 ),
565 "camt.107" => deserialize_doc!(
566 doc_xml,
567 camt_107_001_01::ChequePresentmentNotificationV01,
568 Camt107,
569 "camt.107"
570 ),
571 "camt.108" => deserialize_doc!(
572 doc_xml,
573 camt_108_001_01::ChequeCancellationOrStopRequestV01,
574 Camt108,
575 "camt.108"
576 ),
577 "camt.109" => deserialize_doc!(
578 doc_xml,
579 camt_109_001_01::ChequeCancellationOrStopReportV01,
580 Camt109,
581 "camt.109"
582 ),
583 "admi.024" => deserialize_doc!(
584 doc_xml,
585 admi_024_001_01::NotificationOfCorrespondenceV01,
586 Admi024,
587 "admi.024"
588 ),
589 _ => Err(MxError::XmlDeserialization(format!(
590 "Unsupported message type: {}",
591 message_type
592 ))),
593 }
594 }
595
596 pub fn from_json(json: &str) -> Result<Self, MxError> {
598 let message: MxMessage = serde_json::from_str(json).map_err(|e| {
599 MxError::XmlDeserialization(format!("JSON deserialization failed: {}", e))
600 })?;
601
602 Ok(message)
603 }
604}
605
606pub fn peek_message_type_from_xml(xml: &str) -> Result<String, MxError> {
608 use regex::Regex;
610
611 let re = Regex::new(r"<MsgDefIdr>([^<]+)</MsgDefIdr>")
612 .map_err(|e| MxError::XmlDeserialization(format!("Regex error: {}", e)))?;
613
614 if let Some(captures) = re.captures(xml)
615 && let Some(msg_def_idr) = captures.get(1)
616 {
617 return Ok(normalize_message_type(msg_def_idr.as_str()));
618 }
619
620 Err(MxError::XmlDeserialization(
621 "Could not find MsgDefIdr in XML".to_string(),
622 ))
623}
624
625pub fn peek_message_type_from_json(json: &str) -> Result<String, MxError> {
627 let value: serde_json::Value = serde_json::from_str(json)
628 .map_err(|e| MxError::XmlDeserialization(format!("JSON parsing error: {}", e)))?;
629
630 if let Some(msg_def_idr) = value
632 .get("AppHdr")
633 .or_else(|| value.get("Envelope").and_then(|e| e.get("AppHdr")))
634 .and_then(|hdr| hdr.get("MsgDefIdr"))
635 .and_then(|v| v.as_str())
636 {
637 return Ok(normalize_message_type(msg_def_idr));
638 }
639
640 Err(MxError::XmlDeserialization(
641 "Could not find MsgDefIdr in JSON".to_string(),
642 ))
643}