mx_message/
sample.rs

1// Plasmatic MX Message Parsing Library
2// https://github.com/GoPlasmatic/MXMessage
3//
4// Copyright (c) 2025 Plasmatic
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//     http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16//
17// You may obtain a copy of this library at
18// https://github.com/GoPlasmatic/MXMessage
19
20use crate::error::ValidationError;
21use crate::scenario_config::{
22    ScenarioConfig, find_scenario_by_name_with_config, find_scenario_for_message_type_with_config,
23};
24use crate::xml::to_mx_xml;
25use datafake_rs::DataGenerator;
26use serde_json::{Value, json};
27
28type Result<T> = std::result::Result<T, ValidationError>;
29
30/// Generate a sample MX XML message based on test scenarios
31///
32/// This function loads a test scenario configuration for the specified message type
33/// and generates a complete MX XML message with envelope and header.
34///
35/// # Arguments
36///
37/// * `message_type` - The MX message type (e.g., "pacs008", "camt053")
38/// * `scenario_name` - Optional scenario name. If None, uses the default scenario
39/// * `config` - Configuration for scenario file paths
40///
41/// # Returns
42///
43/// Returns a complete MX XML string with envelope and header
44///
45/// # Example
46///
47/// ```no_run
48/// # use mx_message::sample::generate_sample_xml;
49/// # use mx_message::scenario_config::ScenarioConfig;
50/// // Generate a standard pacs.008 message as XML
51/// let pacs008_xml = generate_sample_xml("pacs008", None, &ScenarioConfig::default()).unwrap();
52///
53/// // Generate a specific scenario
54/// let pacs008_high_value_xml = generate_sample_xml("pacs008", Some("high_value"), &ScenarioConfig::default()).unwrap();
55/// ```
56pub fn generate_sample_xml(
57    message_type: &str,
58    scenario_name: Option<&str>,
59    config: &ScenarioConfig,
60) -> Result<String> {
61    // Load the scenario configuration JSON
62    let scenario_json = if let Some(name) = scenario_name {
63        find_scenario_by_name_with_config(message_type, name, config)?
64    } else {
65        find_scenario_for_message_type_with_config(message_type, config)?
66    };
67
68    // Process the scenario to generate complete envelope using datafake-rs
69    let envelope_data = generate_envelope_from_scenario(&scenario_json)?;
70
71    // Convert envelope to XML based on message type
72    envelope_to_xml(envelope_data, message_type)
73}
74
75/// Generate a sample MX message object based on test scenarios
76///
77/// This function loads a test scenario configuration for the specified message type
78/// and generates the document part of the message as a typed object.
79///
80/// # Arguments
81///
82/// * `message_type` - The MX message type (e.g., "pacs008", "camt053")
83/// * `scenario_name` - Optional scenario name. If None, uses the default scenario
84/// * `config` - Configuration for scenario file paths
85///
86/// # Returns
87///
88/// Returns the document part of the message as a deserializable type
89///
90/// # Example
91///
92/// ```no_run
93/// # use mx_message::sample::generate_sample_object;
94/// # use mx_message::scenario_config::ScenarioConfig;
95/// # use mx_message::document::pacs_008_001_08::FIToFICustomerCreditTransferV08;
96/// // Generate a pacs.008 message object
97/// let pacs008: FIToFICustomerCreditTransferV08 = generate_sample_object("pacs008", None, &ScenarioConfig::default()).unwrap();
98/// ```
99pub fn generate_sample_object<T>(
100    message_type: &str,
101    scenario_name: Option<&str>,
102    config: &ScenarioConfig,
103) -> Result<T>
104where
105    T: serde::de::DeserializeOwned,
106{
107    // Load the scenario configuration JSON
108    let scenario_json = if let Some(name) = scenario_name {
109        find_scenario_by_name_with_config(message_type, name, config)?
110    } else {
111        find_scenario_for_message_type_with_config(message_type, config)?
112    };
113
114    // Process the scenario to generate complete envelope
115    let envelope_data = generate_envelope_from_scenario(&scenario_json)?;
116
117    // Extract the document part based on message type
118    let document_data = extract_document_from_envelope(&envelope_data, message_type)?;
119
120    // Convert generated data to string for parsing
121    let generated_json = serde_json::to_string_pretty(&document_data).map_err(|e| {
122        ValidationError::new(9997, format!("Failed to serialize generated data: {e}"))
123    })?;
124
125    // Parse the generated JSON into the message type
126    serde_json::from_str(&generated_json).map_err(|e| {
127        ValidationError::new(
128            9997,
129            format!("Failed to parse generated JSON into {message_type}: {e}"),
130        )
131    })
132}
133
134// ============================================================================
135// Private implementation functions
136// ============================================================================
137
138/// Generate complete envelope from scenario JSON using datafake-rs
139fn generate_envelope_from_scenario(scenario: &Value) -> Result<Value> {
140    // Convert the scenario format to datafake-rs compatible format
141    let datafake_config = convert_to_datafake_config(scenario)?;
142
143    // Create a DataGenerator from the config
144    let generator = DataGenerator::from_value(datafake_config)
145        .map_err(|e| ValidationError::new(9997, format!("Failed to create generator: {e}")))?;
146
147    // Generate the data
148    generator
149        .generate()
150        .map_err(|e| ValidationError::new(9997, format!("Failed to generate data: {e}")))
151}
152
153/// Convert scenario JSON to datafake-rs compatible configuration
154fn convert_to_datafake_config(scenario: &Value) -> Result<Value> {
155    // Extract variables and schema
156    let variables = scenario.get("variables").ok_or_else(|| {
157        ValidationError::new(9997, "Scenario missing 'variables' section".to_string())
158    })?;
159
160    let schema = scenario.get("schema").ok_or_else(|| {
161        ValidationError::new(9997, "Scenario missing 'schema' section".to_string())
162    })?;
163
164    // Convert the configuration to datafake-rs format
165    // Variables section stays mostly the same, but we need to ensure compatibility
166    let converted_variables = convert_variables(variables)?;
167
168    // Schema section needs to be processed to handle special cases
169    let converted_schema = convert_schema(schema)?;
170
171    Ok(json!({
172        "variables": converted_variables,
173        "schema": converted_schema
174    }))
175}
176
177/// Convert variables section to datafake-rs compatible format
178fn convert_variables(variables: &Value) -> Result<Value> {
179    match variables {
180        Value::Object(vars) => {
181            let mut converted = serde_json::Map::new();
182
183            for (key, spec) in vars {
184                let converted_spec = convert_variable_spec(spec)?;
185                converted.insert(key.clone(), converted_spec);
186            }
187
188            Ok(Value::Object(converted))
189        }
190        _ => Ok(variables.clone()),
191    }
192}
193
194/// Convert a single variable specification
195fn convert_variable_spec(spec: &Value) -> Result<Value> {
196    match spec {
197        Value::Object(obj) => {
198            // Handle cat operations - datafake-rs supports these natively
199            if obj.contains_key("cat") {
200                return Ok(spec.clone());
201            }
202
203            // Handle fake operations - ensure compatibility with datafake-rs
204            if let Some(fake_spec) = obj.get("fake") {
205                if let Value::Array(fake_arr) = fake_spec {
206                    // Map our custom fake types to datafake-rs supported types
207                    if let Some(Value::String(fake_type)) = fake_arr.first() {
208                        let mapped_spec = map_fake_type(fake_type, &fake_arr[1..])?;
209                        return Ok(mapped_spec);
210                    }
211                }
212                return Ok(spec.clone());
213            }
214
215            // Handle pick operations - convert to fake enum
216            if let Some(Value::Array(options)) = obj.get("pick") {
217                // Convert pick to a fake enum operation
218                let mut fake_array = vec![json!("enum")];
219                for option in options {
220                    fake_array.push(option.clone());
221                }
222                return Ok(json!({"fake": fake_array}));
223            }
224
225            Ok(spec.clone())
226        }
227        Value::String(_) | Value::Number(_) | Value::Bool(_) | Value::Null => {
228            // Static values pass through
229            Ok(spec.clone())
230        }
231        _ => Ok(spec.clone()),
232    }
233}
234
235/// Map custom fake types to datafake-rs supported types
236fn map_fake_type(fake_type: &str, args: &[Value]) -> Result<Value> {
237    // Most fake types are compatible, but we need to handle some special cases
238    match fake_type {
239        // ISO datetime is a custom type in our implementation
240        "iso8601_datetime" => {
241            // Since datafake-rs doesn't have datetime support, generate a static ISO datetime
242            let now = chrono::Utc::now();
243            Ok(json!(now.to_rfc3339()))
244        }
245        // Currency code needs mapping
246        "currency_code" => {
247            // Use enum with common currency codes
248            Ok(json!({"fake": ["enum", "EUR", "USD", "GBP", "CHF", "JPY", "CAD", "AUD"]}))
249        }
250        // IBAN generation with country code
251        "iban" => {
252            let country = args.first().and_then(|v| v.as_str()).unwrap_or("DE");
253            Ok(json!({"fake": ["iban", country]}))
254        }
255        // BIC code generation
256        "bic" => Ok(json!({"fake": ["bic"]})),
257        // LEI code generation
258        "lei" => Ok(json!({"fake": ["lei"]})),
259        // Alphanumeric with length
260        "alphanumeric" => {
261            let min_len = args.first().and_then(|v| v.as_u64()).unwrap_or(10);
262            let max_len = args.get(1).and_then(|v| v.as_u64()).unwrap_or(min_len);
263            Ok(json!({"fake": ["alphanumeric", min_len, max_len]}))
264        }
265        // Words generation
266        "words" => {
267            let min = args.first().and_then(|v| v.as_u64()).unwrap_or(1);
268            let max = args.get(1).and_then(|v| v.as_u64()).unwrap_or(3);
269            Ok(json!({"fake": ["words", min, max]}))
270        }
271        // Regex patterns - pass through as-is, datafake-rs now supports it
272        "regex" => {
273            let mut fake_array = vec![json!("regex")];
274            for arg in args {
275                fake_array.push(arg.clone());
276            }
277            Ok(json!({"fake": fake_array}))
278        }
279        // Pass through other types as-is, datafake-rs supports most common ones
280        _ => {
281            let mut fake_array = vec![json!(fake_type)];
282            for arg in args {
283                fake_array.push(arg.clone());
284            }
285            Ok(json!({"fake": fake_array}))
286        }
287    }
288}
289
290/// Convert schema section to datafake-rs compatible format
291fn convert_schema(schema: &Value) -> Result<Value> {
292    process_schema_value(schema)
293}
294
295/// Process schema value recursively
296fn process_schema_value(value: &Value) -> Result<Value> {
297    match value {
298        Value::Object(obj) => {
299            let mut result = serde_json::Map::new();
300
301            for (key, val) in obj {
302                // Handle special keys
303                if key == "@Ccy" {
304                    // This is an attribute in XML, keep as-is
305                    result.insert(key.clone(), process_schema_value(val)?);
306                } else if key == "$value" {
307                    // This is the element value in XML, keep as-is
308                    result.insert(key.clone(), process_schema_value(val)?);
309                } else if key == "var" {
310                    // Variable reference, keep as-is (datafake-rs supports this)
311                    return Ok(value.clone());
312                } else if key == "fake" || key == "cat" || key == "pick" {
313                    // These are datafake-rs operations, process the spec
314                    return convert_variable_spec(value);
315                } else {
316                    // Regular field, process recursively
317                    result.insert(key.clone(), process_schema_value(val)?);
318                }
319            }
320
321            Ok(Value::Object(result))
322        }
323        Value::Array(arr) => {
324            let mut result = Vec::new();
325            for item in arr {
326                result.push(process_schema_value(item)?);
327            }
328            Ok(Value::Array(result))
329        }
330        _ => Ok(value.clone()),
331    }
332}
333
334/// Extract document from envelope for backward compatibility
335fn extract_document_from_envelope(envelope: &Value, message_type: &str) -> Result<Value> {
336    let document = envelope.get("Document").ok_or_else(|| {
337        ValidationError::new(9997, "Envelope missing 'Document' section".to_string())
338    })?;
339
340    // Extract the inner document based on message type
341    let doc_root = get_document_root_element(message_type);
342
343    document
344        .get(&doc_root)
345        .cloned()
346        .ok_or_else(|| ValidationError::new(9997, format!("Document missing '{doc_root}' element")))
347}
348
349/// Get the document root element name for a message type
350fn get_document_root_element(message_type: &str) -> String {
351    match message_type {
352        "pacs008" => "FIToFICstmrCdtTrf",
353        "pacs009" => "FinInstnCdtTrf",
354        "pacs003" => "FIToFICstmrDrctDbt",
355        "pacs002" => "FIToFIPmtStsRpt",
356        "pain001" => "CstmrCdtTrfInitn",
357        "pain008" => "CstmrDrctDbtInitn",
358        "camt025" => "Rcpt",
359        "camt029" => "RsltnOfInvstgtn",
360        "camt052" => "BkToCstmrAcctRpt",
361        "camt053" => "BkToCstmrStmt",
362        "camt054" => "BkToCstmrDbtCdtNtfctn",
363        "camt056" => "FIToFIPmtCxlReq",
364        "camt057" => "NtfctnToRcv",
365        "camt060" => "AcctRptgReq",
366        _ => "Document", // Fallback
367    }
368    .to_string()
369}
370
371/// Convert envelope JSON to typed XML
372fn envelope_to_xml(envelope: Value, message_type: &str) -> Result<String> {
373    // Parse and generate XML based on message type
374    match message_type {
375        "pacs008" => {
376            use crate::document::pacs_008_001_08::FIToFICustomerCreditTransferV08;
377            use crate::header::bah_pacs_008_001_08::BusinessApplicationHeaderV02;
378
379            let header: BusinessApplicationHeaderV02 =
380                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
381                    ValidationError::new(9997, format!("Failed to parse pacs008 header: {e}"))
382                })?;
383
384            let message: FIToFICustomerCreditTransferV08 = serde_json::from_value(
385                envelope["Document"]["FIToFICstmrCdtTrf"].clone(),
386            )
387            .map_err(|e| {
388                ValidationError::new(9997, format!("Failed to parse pacs008 document: {e}"))
389            })?;
390
391            to_mx_xml(&message, header, "pacs.008", None)
392                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
393        }
394        "pacs009" => {
395            use crate::document::pacs_009_001_08::FinancialInstitutionCreditTransferV08;
396            use crate::header::bah_pacs_009_001_08::BusinessApplicationHeaderV02;
397
398            let header: BusinessApplicationHeaderV02 =
399                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
400                    ValidationError::new(9997, format!("Failed to parse pacs009 header: {e}"))
401                })?;
402
403            let message: FinancialInstitutionCreditTransferV08 = serde_json::from_value(
404                envelope["Document"]["FinInstnCdtTrf"].clone(),
405            )
406            .map_err(|e| {
407                ValidationError::new(9997, format!("Failed to parse pacs009 document: {e}"))
408            })?;
409
410            to_mx_xml(&message, header, "pacs.009", None)
411                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
412        }
413        "pacs003" => {
414            use crate::document::pacs_003_001_08::FIToFICustomerDirectDebitV08;
415            use crate::header::bah_pacs_003_001_08::BusinessApplicationHeaderV02;
416
417            let header: BusinessApplicationHeaderV02 =
418                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
419                    ValidationError::new(9997, format!("Failed to parse pacs003 header: {e}"))
420                })?;
421
422            let message: FIToFICustomerDirectDebitV08 =
423                serde_json::from_value(envelope["Document"]["FIToFICstmrDrctDbt"].clone())
424                    .map_err(|e| {
425                        ValidationError::new(9997, format!("Failed to parse pacs003 document: {e}"))
426                    })?;
427
428            to_mx_xml(&message, header, "pacs.003", None)
429                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
430        }
431        "pacs002" => {
432            use crate::document::pacs_002_001_10::FIToFIPaymentStatusReportV10;
433            use crate::header::bah_pacs_002_001_10::BusinessApplicationHeaderV02;
434
435            let header: BusinessApplicationHeaderV02 =
436                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
437                    ValidationError::new(9997, format!("Failed to parse pacs002 header: {e}"))
438                })?;
439
440            let message: FIToFIPaymentStatusReportV10 = serde_json::from_value(
441                envelope["Document"]["FIToFIPmtStsRpt"].clone(),
442            )
443            .map_err(|e| {
444                ValidationError::new(9997, format!("Failed to parse pacs002 document: {e}"))
445            })?;
446
447            to_mx_xml(&message, header, "pacs.002", None)
448                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
449        }
450        "pain001" => {
451            use crate::document::pain_001_001_09::CustomerCreditTransferInitiationV09;
452            use crate::header::bah_pain_001_001_09::BusinessApplicationHeaderV02;
453
454            let header: BusinessApplicationHeaderV02 =
455                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
456                    ValidationError::new(9997, format!("Failed to parse pain001 header: {e}"))
457                })?;
458
459            let message: CustomerCreditTransferInitiationV09 = serde_json::from_value(
460                envelope["Document"]["CstmrCdtTrfInitn"].clone(),
461            )
462            .map_err(|e| {
463                ValidationError::new(9997, format!("Failed to parse pain001 document: {e}"))
464            })?;
465
466            to_mx_xml(&message, header, "pain.001", None)
467                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
468        }
469        "pain008" => {
470            use crate::document::pain_008_001_08::CustomerDirectDebitInitiationV08;
471            use crate::header::bah_pain_008_001_08::BusinessApplicationHeaderV02;
472
473            let header: BusinessApplicationHeaderV02 =
474                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
475                    ValidationError::new(9997, format!("Failed to parse pain008 header: {e}"))
476                })?;
477
478            let message: CustomerDirectDebitInitiationV08 = serde_json::from_value(
479                envelope["Document"]["CstmrDrctDbtInitn"].clone(),
480            )
481            .map_err(|e| {
482                ValidationError::new(9997, format!("Failed to parse pain008 document: {e}"))
483            })?;
484
485            to_mx_xml(&message, header, "pain.008", None)
486                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
487        }
488        "camt025" => {
489            use crate::document::camt_025_001_08::ReceiptV08;
490            use crate::header::bah_camt_025_001_08::BusinessApplicationHeaderV02;
491
492            let header: BusinessApplicationHeaderV02 =
493                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
494                    ValidationError::new(9997, format!("Failed to parse camt025 header: {e}"))
495                })?;
496
497            let message: ReceiptV08 = serde_json::from_value(envelope["Document"]["Rcpt"].clone())
498                .map_err(|e| {
499                    ValidationError::new(9997, format!("Failed to parse camt025 document: {e}"))
500                })?;
501
502            to_mx_xml(&message, header, "camt.025", None)
503                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
504        }
505        "camt029" => {
506            use crate::document::camt_029_001_09::ResolutionOfInvestigationV09;
507            use crate::header::bah_camt_029_001::BusinessApplicationHeaderV02;
508
509            let header: BusinessApplicationHeaderV02 =
510                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
511                    ValidationError::new(9997, format!("Failed to parse camt029 header: {e}"))
512                })?;
513
514            let message: ResolutionOfInvestigationV09 = serde_json::from_value(
515                envelope["Document"]["RsltnOfInvstgtn"].clone(),
516            )
517            .map_err(|e| {
518                ValidationError::new(9997, format!("Failed to parse camt029 document: {e}"))
519            })?;
520
521            to_mx_xml(&message, header, "camt.029", None)
522                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
523        }
524        "camt052" => {
525            use crate::document::camt_052_001_08::BankToCustomerAccountReportV08;
526            use crate::header::bah_camt_052_001_08::BusinessApplicationHeaderV02;
527
528            let header: BusinessApplicationHeaderV02 =
529                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
530                    ValidationError::new(9997, format!("Failed to parse camt052 header: {e}"))
531                })?;
532
533            let message: BankToCustomerAccountReportV08 = serde_json::from_value(
534                envelope["Document"]["BkToCstmrAcctRpt"].clone(),
535            )
536            .map_err(|e| {
537                ValidationError::new(9997, format!("Failed to parse camt052 document: {e}"))
538            })?;
539
540            to_mx_xml(&message, header, "camt.052", None)
541                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
542        }
543        "camt053" => {
544            use crate::document::camt_053_001_08::BankToCustomerStatementV08;
545            use crate::header::bah_camt_053_001_08::BusinessApplicationHeaderV02;
546
547            let header: BusinessApplicationHeaderV02 =
548                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
549                    ValidationError::new(9997, format!("Failed to parse camt053 header: {e}"))
550                })?;
551
552            let message: BankToCustomerStatementV08 = serde_json::from_value(
553                envelope["Document"]["BkToCstmrStmt"].clone(),
554            )
555            .map_err(|e| {
556                ValidationError::new(9997, format!("Failed to parse camt053 document: {e}"))
557            })?;
558
559            to_mx_xml(&message, header, "camt.053", None)
560                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
561        }
562        "camt054" => {
563            use crate::document::camt_054_001_08::BankToCustomerDebitCreditNotificationV08;
564            use crate::header::bah_camt_054_001::BusinessApplicationHeaderV02;
565
566            let header: BusinessApplicationHeaderV02 =
567                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
568                    ValidationError::new(9997, format!("Failed to parse camt054 header: {e}"))
569                })?;
570
571            let message: BankToCustomerDebitCreditNotificationV08 =
572                serde_json::from_value(envelope["Document"]["BkToCstmrDbtCdtNtfctn"].clone())
573                    .map_err(|e| {
574                        ValidationError::new(9997, format!("Failed to parse camt054 document: {e}"))
575                    })?;
576
577            to_mx_xml(&message, header, "camt.054", None)
578                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
579        }
580        "camt056" => {
581            use crate::document::camt_056_001_08::FIToFIPaymentCancellationRequestV08;
582            use crate::header::bah_camt_056_001_08::BusinessApplicationHeaderV02;
583
584            let header: BusinessApplicationHeaderV02 =
585                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
586                    ValidationError::new(9997, format!("Failed to parse camt056 header: {e}"))
587                })?;
588
589            let message: FIToFIPaymentCancellationRequestV08 = serde_json::from_value(
590                envelope["Document"]["FIToFIPmtCxlReq"].clone(),
591            )
592            .map_err(|e| {
593                ValidationError::new(9997, format!("Failed to parse camt056 document: {e}"))
594            })?;
595
596            to_mx_xml(&message, header, "camt.056", None)
597                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
598        }
599        "camt057" => {
600            use crate::document::camt_057_001_06::NotificationToReceiveV06;
601            use crate::header::bah_camt_057_001_06::BusinessApplicationHeaderV02;
602
603            let header: BusinessApplicationHeaderV02 =
604                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
605                    ValidationError::new(9997, format!("Failed to parse camt057 header: {e}"))
606                })?;
607
608            let message: NotificationToReceiveV06 = serde_json::from_value(
609                envelope["Document"]["NtfctnToRcv"].clone(),
610            )
611            .map_err(|e| {
612                ValidationError::new(9997, format!("Failed to parse camt057 document: {e}"))
613            })?;
614
615            to_mx_xml(&message, header, "camt.057", None)
616                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
617        }
618        "camt060" => {
619            use crate::document::camt_060_001_05::AccountReportingRequestV05;
620            use crate::header::bah_camt_060_001_05::BusinessApplicationHeaderV02;
621
622            let header: BusinessApplicationHeaderV02 =
623                serde_json::from_value(envelope["AppHdr"].clone()).map_err(|e| {
624                    ValidationError::new(9997, format!("Failed to parse camt060 header: {e}"))
625                })?;
626
627            let message: AccountReportingRequestV05 = serde_json::from_value(
628                envelope["Document"]["AcctRptgReq"].clone(),
629            )
630            .map_err(|e| {
631                ValidationError::new(9997, format!("Failed to parse camt060 document: {e}"))
632            })?;
633
634            to_mx_xml(&message, header, "camt.060", None)
635                .map_err(|e| ValidationError::new(9997, format!("Failed to generate XML: {e}")))
636        }
637        _ => Err(ValidationError::new(
638            9997,
639            format!("Unsupported message type: {message_type}"),
640        )),
641    }
642}