mx_message/
mx_envelope.rs

1// MX Message Envelope Structure for ISO 20022 compliant XML generation
2
3use serde::{Deserialize, Serialize};
4
5// Re-export AppHdr for convenience
6use crate::error::MxError;
7pub use crate::header::AppHdr;
8use crate::message_registry;
9
10/// Document enum - represents the Document element in MX messages
11/// Each variant uses serde rename to match the XML element name
12/// Box wrappers are used to prevent stack overflow from large variants
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub enum Document {
15    // PACS - Payment Clearing and Settlement
16    #[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    // PAIN - Payment Initiation
35    #[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    // CAMT - Cash Management
45    #[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    // ADMI - Administration
91    #[serde(rename = "NtfctnOfCrspdc")]
92    Admi024(Box<crate::document::admi_024_001_01::NotificationOfCorrespondenceV01>),
93}
94
95impl Document {
96    /// Get the namespace for this document based on its type
97    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/// Complete MX message containing Business Application Header and Document
130/// This is the unified structure for all ISO20022 message types
131/// The message type is determined from the AppHdr.MsgDefIdr field
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
133#[serde(rename = "Envelope")]
134pub struct MxMessage {
135    /// XML namespace declarations
136    #[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    /// Business Application Header
143    #[serde(rename = "AppHdr")]
144    pub app_hdr: crate::header::AppHdr,
145
146    /// Document containing the actual message
147    #[serde(rename = "Document")]
148    pub document: Document,
149}
150
151impl MxMessage {
152    /// Create a new MX message with default namespaces
153    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
163/// Get the appropriate namespace for a message type
164/// Delegates to message_registry module
165pub fn get_namespace_for_message_type(message_type: &str) -> String {
166    message_registry::get_namespace(message_type)
167}
168
169/// Get the short form of message type (e.g., "pacs.008")
170/// Delegates to message_registry module
171pub fn normalize_message_type(message_type: &str) -> String {
172    message_registry::normalize_message_type(message_type)
173}
174
175/// Macro to reduce serialization boilerplate
176macro_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
182/// Macro to reduce deserialization boilerplate
183macro_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    /// Get the message type identifier from AppHdr (e.g., "pacs.008.001.08")
194    pub fn message_type(&self) -> Result<&str, MxError> {
195        Ok(&self.app_hdr.msg_def_idr)
196    }
197
198    /// Get the namespace for this message
199    pub fn namespace(&self) -> Result<String, MxError> {
200        Ok(get_namespace_for_message_type(self.message_type()?))
201    }
202
203    /// Helper function to serialize a document with struct name replacement
204    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    /// Serialize to XML string
219    pub fn to_xml(&self) -> Result<String, MxError> {
220        // Custom serialization to handle enum variants
221        // Serialize AppHdr
222        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        // AppHdr is serialized directly without a wrapper tag, use it as-is
226        let app_hdr_inner = app_hdr_xml;
227
228        // Serialize Document based on its variant
229        let doc_xml = self.serialize_document()?;
230
231        // Build complete envelope
232        // Note: quick-xml adds struct name as wrapper, so app_hdr_inner already contains <BusinessApplicationHeaderV02>
233        // We need to rename it to <AppHdr>
234        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    /// Serialize document based on its variant
250    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    /// Serialize to JSON string
387    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    /// Deserialize from XML string using quick-xml with custom enum handling
392    pub fn from_xml(xml: &str) -> Result<Self, MxError> {
393        // Check if XML contains full envelope or just Document
394        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    /// Deserialize XML with full envelope (AppHdr + Document)
404    fn from_xml_with_envelope(xml: &str) -> Result<Self, MxError> {
405        // Extract AppHdr section
406        let app_hdr_xml = Self::extract_section(xml, "AppHdr")
407            .ok_or_else(|| MxError::XmlDeserialization("AppHdr not found in XML".to_string()))?;
408
409        // Deserialize AppHdr using quick-xml
410        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        // Extract Document section
416        let doc_xml = Self::extract_section(xml, "Document")
417            .ok_or_else(|| MxError::XmlDeserialization("Document not found in XML".to_string()))?;
418
419        // Determine document type from the first element inside Document
420        let doc_type = Self::detect_document_type(&doc_xml)?;
421
422        // Deserialize the document based on its type
423        let document = Self::deserialize_document(&doc_xml, &doc_type)?;
424
425        // Extract namespace attributes if present
426        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    /// Deserialize document-only XML (no envelope)
438    fn from_xml_document_only(_xml: &str) -> Result<Self, MxError> {
439        // For document-only XML, we need to create a minimal AppHdr
440        // This is a fallback case - typically we expect full envelopes
441        Err(MxError::XmlDeserialization(
442            "Document-only XML requires AppHdr information. Use full envelope format.".to_string(),
443        ))
444    }
445
446    /// Extract content between XML tags
447    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    /// Extract XML attribute value
463    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    /// Detect document type from XML content
471    fn detect_document_type(doc_xml: &str) -> Result<String, MxError> {
472        // Find the first element tag after any whitespace
473        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        // Map element name to message type using message registry
490        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    /// Deserialize document based on message type
499    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    /// Deserialize from JSON string
658    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
667/// Extract message type from XML without full deserialization
668pub fn peek_message_type_from_xml(xml: &str) -> Result<String, MxError> {
669    // Simple regex-based extraction for MsgDefIdr
670    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
686/// Extract message type from JSON without full deserialization
687pub 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    // Try to extract MsgDefIdr from AppHdr
692    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}