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 = "CstmrPmtCxlReq")]
61 Camt055(Box<crate::document::camt_055_001_08::CustomerPaymentCancellationRequestV08>),
62
63 #[serde(rename = "FIToFIPmtCxlReq")]
64 Camt056(Box<crate::document::camt_056_001_08::FIToFIPaymentCancellationRequestV08>),
65
66 #[serde(rename = "NtfctnToRcvCxlAdvc")]
67 Camt058(Box<crate::document::camt_058_001_08::NotificationToReceiveCancellationAdviceV08>),
68
69 #[serde(rename = "NtfctnToRcv")]
70 Camt057(Box<crate::document::camt_057_001_06::NotificationToReceiveV06>),
71
72 #[serde(rename = "AcctRptgReq")]
73 Camt060(Box<crate::document::camt_060_001_05::AccountReportingRequestV05>),
74
75 #[serde(rename = "ChrgsPmtNtfctn")]
76 Camt105(Box<crate::document::camt_105_001_02::ChargesPaymentNotificationV02>),
77
78 #[serde(rename = "ChrgsPmtReq")]
79 Camt106(Box<crate::document::camt_106_001_02::ChargesPaymentRequestV02>),
80
81 #[serde(rename = "ChqPresntmntNtfctn")]
82 Camt107(Box<crate::document::camt_107_001_01::ChequePresentmentNotificationV01>),
83
84 #[serde(rename = "ChqCxlOrStopReq")]
85 Camt108(Box<crate::document::camt_108_001_01::ChequeCancellationOrStopRequestV01>),
86
87 #[serde(rename = "ChqCxlOrStopRpt")]
88 Camt109(Box<crate::document::camt_109_001_01::ChequeCancellationOrStopReportV01>),
89
90 #[serde(rename = "NtfctnOfCrspdc")]
92 Admi024(Box<crate::document::admi_024_001_01::NotificationOfCorrespondenceV01>),
93}
94
95impl Document {
96 pub fn namespace(&self) -> String {
98 let msg_type = match self {
99 Document::Pacs008(_) => "pacs.008",
100 Document::Pacs009(_) => "pacs.009",
101 Document::Pacs003(_) => "pacs.003",
102 Document::Pacs004(_) => "pacs.004",
103 Document::Pacs002(_) => "pacs.002",
104 Document::Pacs010(_) => "pacs.010",
105 Document::Pain001(_) => "pain.001",
106 Document::Pain002(_) => "pain.002",
107 Document::Pain008(_) => "pain.008",
108 Document::Camt025(_) => "camt.025",
109 Document::Camt029(_) => "camt.029",
110 Document::Camt052(_) => "camt.052",
111 Document::Camt053(_) => "camt.053",
112 Document::Camt054(_) => "camt.054",
113 Document::Camt055(_) => "camt.055",
114 Document::Camt056(_) => "camt.056",
115 Document::Camt057(_) => "camt.057",
116 Document::Camt058(_) => "camt.058",
117 Document::Camt060(_) => "camt.060",
118 Document::Camt105(_) => "camt.105",
119 Document::Camt106(_) => "camt.106",
120 Document::Camt107(_) => "camt.107",
121 Document::Camt108(_) => "camt.108",
122 Document::Camt109(_) => "camt.109",
123 Document::Admi024(_) => "admi.024",
124 };
125 message_registry::get_namespace(msg_type)
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
133#[serde(rename = "Envelope")]
134pub struct MxMessage {
135 #[serde(rename = "@xmlns", skip_serializing_if = "Option::is_none")]
137 pub xmlns: Option<String>,
138
139 #[serde(rename = "@xmlns:xsi", skip_serializing_if = "Option::is_none")]
140 pub xmlns_xsi: Option<String>,
141
142 #[serde(rename = "AppHdr")]
144 pub app_hdr: crate::header::AppHdr,
145
146 #[serde(rename = "Document")]
148 pub document: Document,
149}
150
151impl MxMessage {
152 pub fn new(app_hdr: crate::header::AppHdr, document: Document) -> Self {
154 Self {
155 xmlns: Some("urn:iso:std:iso:20022:tech:xsd:head.001.001.02".to_string()),
156 xmlns_xsi: Some("http://www.w3.org/2001/XMLSchema-instance".to_string()),
157 app_hdr,
158 document,
159 }
160 }
161}
162
163pub fn get_namespace_for_message_type(message_type: &str) -> String {
166 message_registry::get_namespace(message_type)
167}
168
169pub fn normalize_message_type(message_type: &str) -> String {
172 message_registry::normalize_message_type(message_type)
173}
174
175macro_rules! serialize_doc {
177 ($doc:expr, $rust_type:expr, $xml_elem:expr, $msg_type:expr) => {
178 MxMessage::serialize_with_rename($doc.as_ref(), $rust_type, $xml_elem, $msg_type)
179 };
180}
181
182macro_rules! deserialize_doc {
184 ($xml:expr, $path:path, $variant:ident, $msg_type:expr) => {{
185 let doc = quick_xml::de::from_str::<$path>($xml).map_err(|e| {
186 MxError::XmlDeserialization(format!("Failed to parse {}: {}", $msg_type, e))
187 })?;
188 Ok(Document::$variant(Box::new(doc)))
189 }};
190}
191
192impl MxMessage {
193 pub fn message_type(&self) -> Result<&str, MxError> {
195 Ok(&self.app_hdr.msg_def_idr)
196 }
197
198 pub fn namespace(&self) -> Result<String, MxError> {
200 Ok(get_namespace_for_message_type(self.message_type()?))
201 }
202
203 fn serialize_with_rename<T: Serialize>(
205 value: &T,
206 rust_type: &str,
207 xml_element: &str,
208 msg_type: &str,
209 ) -> Result<String, MxError> {
210 let xml = quick_xml::se::to_string(value).map_err(|e| {
211 MxError::XmlSerialization(format!("Failed to serialize {}: {}", msg_type, e))
212 })?;
213 Ok(xml
214 .replace(&format!("<{}>", rust_type), &format!("<{}>", xml_element))
215 .replace(&format!("</{}>", rust_type), &format!("</{}>", xml_element)))
216 }
217
218 pub fn to_xml(&self) -> Result<String, MxError> {
220 let app_hdr_xml = quick_xml::se::to_string(&self.app_hdr)
223 .map_err(|e| MxError::XmlSerialization(format!("Failed to serialize AppHdr: {}", e)))?;
224
225 let app_hdr_inner = app_hdr_xml;
227
228 let doc_xml = self.serialize_document()?;
230
231 let app_hdr_wrapped = app_hdr_inner
235 .replace("<BusinessApplicationHeaderV02>", "<AppHdr>")
236 .replace("</BusinessApplicationHeaderV02>", "</AppHdr>");
237
238 let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
239 xml.push_str("<Envelope>");
240 xml.push_str(&app_hdr_wrapped);
241 xml.push_str("<Document>");
242 xml.push_str(&doc_xml);
243 xml.push_str("</Document>");
244 xml.push_str("</Envelope>");
245
246 Ok(xml)
247 }
248
249 fn serialize_document(&self) -> Result<String, MxError> {
251 match &self.document {
252 Document::Pacs008(doc) => serialize_doc!(
253 doc,
254 "FIToFICustomerCreditTransferV08",
255 "FIToFICstmrCdtTrf",
256 "pacs.008"
257 ),
258 Document::Pacs002(doc) => serialize_doc!(
259 doc,
260 "FIToFIPaymentStatusReportV10",
261 "FIToFIPmtStsRpt",
262 "pacs.002"
263 ),
264 Document::Pacs003(doc) => serialize_doc!(
265 doc,
266 "FIToFICustomerDirectDebitV08",
267 "FIToFICstmrDrctDbt",
268 "pacs.003"
269 ),
270 Document::Pacs004(doc) => serialize_doc!(doc, "PaymentReturnV09", "PmtRtr", "pacs.004"),
271 Document::Pacs009(doc) => serialize_doc!(
272 doc,
273 "FinancialInstitutionCreditTransferV08",
274 "FICdtTrf",
275 "pacs.009"
276 ),
277 Document::Pacs010(doc) => serialize_doc!(
278 doc,
279 "FinancialInstitutionDirectDebitV03",
280 "FIDrctDbt",
281 "pacs.010"
282 ),
283 Document::Pain001(doc) => serialize_doc!(
284 doc,
285 "CustomerCreditTransferInitiationV09",
286 "CstmrCdtTrfInitn",
287 "pain.001"
288 ),
289 Document::Pain002(doc) => serialize_doc!(
290 doc,
291 "CustomerPaymentStatusReportV10",
292 "CstmrPmtStsRpt",
293 "pain.002"
294 ),
295 Document::Pain008(doc) => serialize_doc!(
296 doc,
297 "CustomerDirectDebitInitiationV08",
298 "CstmrDrctDbtInitn",
299 "pain.008"
300 ),
301 Document::Camt025(doc) => serialize_doc!(doc, "ReceiptV08", "Rcpt", "camt.025"),
302 Document::Camt029(doc) => serialize_doc!(
303 doc,
304 "ResolutionOfInvestigationV09",
305 "RsltnOfInvstgtn",
306 "camt.029"
307 ),
308 Document::Camt052(doc) => serialize_doc!(
309 doc,
310 "BankToCustomerAccountReportV08",
311 "BkToCstmrAcctRpt",
312 "camt.052"
313 ),
314 Document::Camt053(doc) => serialize_doc!(
315 doc,
316 "BankToCustomerStatementV08",
317 "BkToCstmrStmt",
318 "camt.053"
319 ),
320 Document::Camt054(doc) => serialize_doc!(
321 doc,
322 "BankToCustomerDebitCreditNotificationV08",
323 "BkToCstmrDbtCdtNtfctn",
324 "camt.054"
325 ),
326 Document::Camt055(doc) => serialize_doc!(
327 doc,
328 "CustomerPaymentCancellationRequestV08",
329 "CstmrPmtCxlReq",
330 "camt.055"
331 ),
332 Document::Camt056(doc) => serialize_doc!(
333 doc,
334 "FIToFIPaymentCancellationRequestV08",
335 "FIToFIPmtCxlReq",
336 "camt.056"
337 ),
338 Document::Camt058(doc) => serialize_doc!(
339 doc,
340 "NotificationToReceiveCancellationAdviceV08",
341 "NtfctnToRcvCxlAdvc",
342 "camt.058"
343 ),
344 Document::Camt057(doc) => {
345 serialize_doc!(doc, "NotificationToReceiveV06", "NtfctnToRcv", "camt.057")
346 }
347 Document::Camt060(doc) => {
348 serialize_doc!(doc, "AccountReportingRequestV05", "AcctRptgReq", "camt.060")
349 }
350 Document::Camt105(doc) => serialize_doc!(
351 doc,
352 "ChargesPaymentNotificationV02",
353 "ChrgsPmtNtfctn",
354 "camt.105"
355 ),
356 Document::Camt106(doc) => {
357 serialize_doc!(doc, "ChargesPaymentRequestV02", "ChrgsPmtReq", "camt.106")
358 }
359 Document::Camt107(doc) => serialize_doc!(
360 doc,
361 "ChequePresentmentNotificationV01",
362 "ChqPresntmntNtfctn",
363 "camt.107"
364 ),
365 Document::Camt108(doc) => serialize_doc!(
366 doc,
367 "ChequeCancellationOrStopRequestV01",
368 "ChqCxlOrStopReq",
369 "camt.108"
370 ),
371 Document::Camt109(doc) => serialize_doc!(
372 doc,
373 "ChequeCancellationOrStopReportV01",
374 "ChqCxlOrStopRpt",
375 "camt.109"
376 ),
377 Document::Admi024(doc) => serialize_doc!(
378 doc,
379 "NotificationOfCorrespondenceV01",
380 "NtfctnOfCrspdc",
381 "admi.024"
382 ),
383 }
384 }
385
386 pub fn to_json(&self) -> Result<String, MxError> {
388 serde_json::to_string_pretty(self).map_err(|e| MxError::XmlSerialization(e.to_string()))
389 }
390
391 pub fn from_xml(xml: &str) -> Result<Self, MxError> {
393 let has_envelope = xml.contains("<AppHdr") || xml.contains("<Envelope");
395
396 if has_envelope {
397 Self::from_xml_with_envelope(xml)
398 } else {
399 Self::from_xml_document_only(xml)
400 }
401 }
402
403 fn from_xml_with_envelope(xml: &str) -> Result<Self, MxError> {
405 let app_hdr_xml = Self::extract_section(xml, "AppHdr")
407 .ok_or_else(|| MxError::XmlDeserialization("AppHdr not found in XML".to_string()))?;
408
409 let app_hdr: crate::header::AppHdr =
411 quick_xml::de::from_str(&format!("<AppHdr>{}</AppHdr>", app_hdr_xml)).map_err(|e| {
412 MxError::XmlDeserialization(format!("Failed to parse AppHdr: {}", e))
413 })?;
414
415 let doc_xml = Self::extract_section(xml, "Document")
417 .ok_or_else(|| MxError::XmlDeserialization("Document not found in XML".to_string()))?;
418
419 let doc_type = Self::detect_document_type(&doc_xml)?;
421
422 let document = Self::deserialize_document(&doc_xml, &doc_type)?;
424
425 let xmlns = Self::extract_attribute(xml, "xmlns");
427 let xmlns_xsi = Self::extract_attribute(xml, "xmlns:xsi");
428
429 Ok(MxMessage {
430 xmlns,
431 xmlns_xsi,
432 app_hdr,
433 document,
434 })
435 }
436
437 fn from_xml_document_only(_xml: &str) -> Result<Self, MxError> {
439 Err(MxError::XmlDeserialization(
442 "Document-only XML requires AppHdr information. Use full envelope format.".to_string(),
443 ))
444 }
445
446 fn extract_section(xml: &str, tag: &str) -> Option<String> {
448 let start_tag = format!("<{}", tag);
449 let end_tag = format!("</{}>", tag);
450
451 let start_idx = xml.find(&start_tag)?;
452 let content_start = xml[start_idx..].find('>')? + start_idx + 1;
453 let end_idx = xml.find(&end_tag)?;
454
455 if content_start < end_idx {
456 Some(xml[content_start..end_idx].to_string())
457 } else {
458 None
459 }
460 }
461
462 fn extract_attribute(xml: &str, attr: &str) -> Option<String> {
464 let pattern = format!("{}=\"", attr);
465 let start_idx = xml.find(&pattern)? + pattern.len();
466 let end_idx = xml[start_idx..].find('"')? + start_idx;
467 Some(xml[start_idx..end_idx].to_string())
468 }
469
470 fn detect_document_type(doc_xml: &str) -> Result<String, MxError> {
472 let trimmed = doc_xml.trim();
474 if !trimmed.starts_with('<') {
475 return Err(MxError::XmlDeserialization(
476 "Invalid document XML structure".to_string(),
477 ));
478 }
479
480 let end_idx = trimmed[1..]
481 .find(|c: char| c.is_whitespace() || c == '>')
482 .map(|i| i + 1)
483 .ok_or_else(|| {
484 MxError::XmlDeserialization("Could not find document element".to_string())
485 })?;
486
487 let element_name = &trimmed[1..end_idx];
488
489 let message_type =
491 message_registry::element_to_message_type(element_name).ok_or_else(|| {
492 MxError::XmlDeserialization(format!("Unknown document type: {}", element_name))
493 })?;
494
495 Ok(message_type.to_string())
496 }
497
498 fn deserialize_document(doc_xml: &str, message_type: &str) -> Result<Document, MxError> {
500 use crate::document::*;
501
502 match message_type {
503 "pacs.008" => deserialize_doc!(
504 doc_xml,
505 pacs_008_001_08::FIToFICustomerCreditTransferV08,
506 Pacs008,
507 "pacs.008"
508 ),
509 "pacs.002" => deserialize_doc!(
510 doc_xml,
511 pacs_002_001_10::FIToFIPaymentStatusReportV10,
512 Pacs002,
513 "pacs.002"
514 ),
515 "pacs.003" => deserialize_doc!(
516 doc_xml,
517 pacs_003_001_08::FIToFICustomerDirectDebitV08,
518 Pacs003,
519 "pacs.003"
520 ),
521 "pacs.004" => deserialize_doc!(
522 doc_xml,
523 pacs_004_001_09::PaymentReturnV09,
524 Pacs004,
525 "pacs.004"
526 ),
527 "pacs.009" => deserialize_doc!(
528 doc_xml,
529 pacs_009_001_08::FinancialInstitutionCreditTransferV08,
530 Pacs009,
531 "pacs.009"
532 ),
533 "pacs.010" => deserialize_doc!(
534 doc_xml,
535 pacs_010_001_03::FinancialInstitutionDirectDebitV03,
536 Pacs010,
537 "pacs.010"
538 ),
539 "pain.001" => deserialize_doc!(
540 doc_xml,
541 pain_001_001_09::CustomerCreditTransferInitiationV09,
542 Pain001,
543 "pain.001"
544 ),
545 "pain.002" => deserialize_doc!(
546 doc_xml,
547 pain_002_001_10::CustomerPaymentStatusReportV10,
548 Pain002,
549 "pain.002"
550 ),
551 "pain.008" => deserialize_doc!(
552 doc_xml,
553 pain_008_001_08::CustomerDirectDebitInitiationV08,
554 Pain008,
555 "pain.008"
556 ),
557 "camt.025" => {
558 deserialize_doc!(doc_xml, camt_025_001_08::ReceiptV08, Camt025, "camt.025")
559 }
560 "camt.029" => deserialize_doc!(
561 doc_xml,
562 camt_029_001_09::ResolutionOfInvestigationV09,
563 Camt029,
564 "camt.029"
565 ),
566 "camt.052" => deserialize_doc!(
567 doc_xml,
568 camt_052_001_08::BankToCustomerAccountReportV08,
569 Camt052,
570 "camt.052"
571 ),
572 "camt.053" => deserialize_doc!(
573 doc_xml,
574 camt_053_001_08::BankToCustomerStatementV08,
575 Camt053,
576 "camt.053"
577 ),
578 "camt.054" => deserialize_doc!(
579 doc_xml,
580 camt_054_001_08::BankToCustomerDebitCreditNotificationV08,
581 Camt054,
582 "camt.054"
583 ),
584 "camt.055" => deserialize_doc!(
585 doc_xml,
586 camt_055_001_08::CustomerPaymentCancellationRequestV08,
587 Camt055,
588 "camt.055"
589 ),
590 "camt.056" => deserialize_doc!(
591 doc_xml,
592 camt_056_001_08::FIToFIPaymentCancellationRequestV08,
593 Camt056,
594 "camt.056"
595 ),
596 "camt.058" => deserialize_doc!(
597 doc_xml,
598 camt_058_001_08::NotificationToReceiveCancellationAdviceV08,
599 Camt058,
600 "camt.058"
601 ),
602 "camt.057" => deserialize_doc!(
603 doc_xml,
604 camt_057_001_06::NotificationToReceiveV06,
605 Camt057,
606 "camt.057"
607 ),
608 "camt.060" => deserialize_doc!(
609 doc_xml,
610 camt_060_001_05::AccountReportingRequestV05,
611 Camt060,
612 "camt.060"
613 ),
614 "camt.105" => deserialize_doc!(
615 doc_xml,
616 camt_105_001_02::ChargesPaymentNotificationV02,
617 Camt105,
618 "camt.105"
619 ),
620 "camt.106" => deserialize_doc!(
621 doc_xml,
622 camt_106_001_02::ChargesPaymentRequestV02,
623 Camt106,
624 "camt.106"
625 ),
626 "camt.107" => deserialize_doc!(
627 doc_xml,
628 camt_107_001_01::ChequePresentmentNotificationV01,
629 Camt107,
630 "camt.107"
631 ),
632 "camt.108" => deserialize_doc!(
633 doc_xml,
634 camt_108_001_01::ChequeCancellationOrStopRequestV01,
635 Camt108,
636 "camt.108"
637 ),
638 "camt.109" => deserialize_doc!(
639 doc_xml,
640 camt_109_001_01::ChequeCancellationOrStopReportV01,
641 Camt109,
642 "camt.109"
643 ),
644 "admi.024" => deserialize_doc!(
645 doc_xml,
646 admi_024_001_01::NotificationOfCorrespondenceV01,
647 Admi024,
648 "admi.024"
649 ),
650 _ => Err(MxError::XmlDeserialization(format!(
651 "Unsupported message type: {}",
652 message_type
653 ))),
654 }
655 }
656
657 pub fn from_json(json: &str) -> Result<Self, MxError> {
659 let message: MxMessage = serde_json::from_str(json).map_err(|e| {
660 MxError::XmlDeserialization(format!("JSON deserialization failed: {}", e))
661 })?;
662
663 Ok(message)
664 }
665}
666
667pub fn peek_message_type_from_xml(xml: &str) -> Result<String, MxError> {
669 use regex::Regex;
671
672 let re = Regex::new(r"<MsgDefIdr>([^<]+)</MsgDefIdr>")
673 .map_err(|e| MxError::XmlDeserialization(format!("Regex error: {}", e)))?;
674
675 if let Some(captures) = re.captures(xml)
676 && let Some(msg_def_idr) = captures.get(1)
677 {
678 return Ok(normalize_message_type(msg_def_idr.as_str()));
679 }
680
681 Err(MxError::XmlDeserialization(
682 "Could not find MsgDefIdr in XML".to_string(),
683 ))
684}
685
686pub fn peek_message_type_from_json(json: &str) -> Result<String, MxError> {
688 let value: serde_json::Value = serde_json::from_str(json)
689 .map_err(|e| MxError::XmlDeserialization(format!("JSON parsing error: {}", e)))?;
690
691 if let Some(msg_def_idr) = value
693 .get("AppHdr")
694 .or_else(|| value.get("Envelope").and_then(|e| e.get("AppHdr")))
695 .and_then(|hdr| hdr.get("MsgDefIdr"))
696 .and_then(|v| v.as_str())
697 {
698 return Ok(normalize_message_type(msg_def_idr));
699 }
700
701 Err(MxError::XmlDeserialization(
702 "Could not find MsgDefIdr in JSON".to_string(),
703 ))
704}