Skip to main content

iso8583_codec_rs/
lib.rs

1pub mod bitmap;
2pub mod codec;
3pub mod error;
4pub mod field_spec;
5pub mod iso8583;
6pub mod mti;
7pub mod validation;
8
9pub use error::{Iso8583Error, Result};
10pub use field_spec::LoadedSpec;
11pub use validation::{ValidationError, ValidationResult};
12
13/// Parse an ISO 8583 message from a hex-encoded string into a JSON value.
14///
15/// The spec defines the field layout (encoding, prefix, padding, composite types).
16/// Returns a JSON object with `mti` and `fields` keys.
17pub fn parse(hex_message: &str, spec: &LoadedSpec) -> Result<serde_json::Value> {
18    iso8583::parse(hex_message, spec)
19}
20
21/// Serialize a JSON value into a hex-encoded ISO 8583 message string.
22///
23/// The JSON must have `mti` (string) and `fields` (object with `de{NNN}_{name}` keys).
24pub fn publish(message: &serde_json::Value, spec: &LoadedSpec) -> Result<String> {
25    iso8583::publish(message, spec)
26}
27
28/// Load a specification from a JSON string.
29pub fn load_spec(json: &str) -> Result<LoadedSpec> {
30    LoadedSpec::from_json(json)
31}
32
33/// Validate an ISO 8583 message against the specification.
34///
35/// Checks MTI format, mandatory fields, per-field format/length/pattern, and response codes.
36/// Returns a `ValidationResult` containing all errors found (non-fatal, accumulated).
37pub fn validate(message: &serde_json::Value, spec: &LoadedSpec) -> ValidationResult {
38    validation::validate(message, spec)
39}
40
41/// Look up a DE 39 response code description.
42///
43/// Returns `Some(description)` for known codes (e.g. `"00"` → `"Approved"`), `None` otherwise.
44pub fn lookup_response_code(code: &str) -> Option<&'static str> {
45    validation::lookup_response_code(code)
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use serde_json::json;
52
53    fn default_spec() -> LoadedSpec {
54        let spec_json = include_str!("../specifications/fields.json");
55        load_spec(spec_json).expect("failed to load spec")
56    }
57
58    #[test]
59    fn test_load_spec() {
60        let spec = default_spec();
61        assert_eq!(spec.version, "1987");
62        assert!(spec.get_field(2).is_some());
63        assert!(spec.get_field(3).is_some());
64        assert!(spec.get_field(128).is_some());
65    }
66
67    #[test]
68    fn test_simple_round_trip() {
69        let spec = default_spec();
70
71        let message = json!({
72            "mti": "0200",
73            "fields": {
74                "de002_primary_account_number": "4111111111111111",
75                "de003_processing_code": "000000",
76                "de004_amount_transaction": "000000001000",
77                "de011_system_trace_audit_number": "123456",
78                "de041_card_acceptor_terminal_id": "TERM0001"
79            }
80        });
81
82        let hex_encoded = publish(&message, &spec).expect("publish failed");
83        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
84
85        assert_eq!(parsed["mti"], "0200");
86        assert_eq!(
87            parsed["fields"]["de002_primary_account_number"],
88            "4111111111111111"
89        );
90        assert_eq!(parsed["fields"]["de003_processing_code"], "000000");
91        assert_eq!(parsed["fields"]["de004_amount_transaction"], "000000001000");
92        assert_eq!(
93            parsed["fields"]["de011_system_trace_audit_number"],
94            "123456"
95        );
96        assert_eq!(
97            parsed["fields"]["de041_card_acceptor_terminal_id"],
98            "TERM0001"
99        );
100    }
101
102    #[test]
103    fn test_round_trip_with_llvar_fields() {
104        let spec = default_spec();
105
106        let message = json!({
107            "mti": "0200",
108            "fields": {
109                "de002_primary_account_number": "4111111111111111",
110                "de003_processing_code": "000000",
111                "de004_amount_transaction": "000000001000",
112                "de007_transmission_date_time": "0725143052",
113                "de011_system_trace_audit_number": "123456",
114                "de032_acquiring_institution_id_code": "123456",
115                "de035_track_2_data": "4111111111111111D2512101123400001",
116                "de037_retrieval_reference_number": "425612000001",
117                "de041_card_acceptor_terminal_id": "TERM0001",
118                "de043_card_acceptor_name_location": "ACME STORE          NEW YORK     NY US"
119            }
120        });
121
122        let hex_encoded = publish(&message, &spec).expect("publish failed");
123        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
124
125        assert_eq!(parsed["mti"], "0200");
126        assert_eq!(
127            parsed["fields"]["de002_primary_account_number"],
128            "4111111111111111"
129        );
130        assert_eq!(
131            parsed["fields"]["de035_track_2_data"],
132            "4111111111111111D2512101123400001"
133        );
134        assert_eq!(
135            parsed["fields"]["de037_retrieval_reference_number"],
136            "425612000001"
137        );
138        // Fixed-length field should be padded to 40 chars
139        let name_loc = parsed["fields"]["de043_card_acceptor_name_location"]
140            .as_str()
141            .unwrap();
142        assert_eq!(name_loc.len(), 40);
143    }
144
145    #[test]
146    fn test_composite_ordered_field() {
147        let spec = default_spec();
148
149        let message = json!({
150            "mti": "0200",
151            "fields": {
152                "de003_processing_code": "000000",
153                "de022_pos_entry_mode": {
154                    "pan_entry_mode": "05",
155                    "pin_entry_capability": "1"
156                }
157            }
158        });
159
160        let hex_encoded = publish(&message, &spec).expect("publish failed");
161        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
162
163        assert_eq!(parsed["mti"], "0200");
164        let pos = &parsed["fields"]["de022_pos_entry_mode"];
165        assert_eq!(pos["pan_entry_mode"], "05");
166        assert_eq!(pos["pin_entry_capability"], "1");
167    }
168
169    #[test]
170    fn test_composite_tlv_field() {
171        let spec = default_spec();
172
173        let message = json!({
174            "mti": "0200",
175            "fields": {
176                "de003_processing_code": "000000",
177                "de055_emv_data": {
178                    "9F26": "AABBCCDD11223344",
179                    "9F27": "80",
180                    "9F10": "0102030405"
181                }
182            }
183        });
184
185        let hex_encoded = publish(&message, &spec).expect("publish failed");
186        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
187
188        assert_eq!(parsed["mti"], "0200");
189        let emv = &parsed["fields"]["de055_emv_data"];
190        assert_eq!(emv["9F26"], "AABBCCDD11223344");
191        assert_eq!(emv["9F27"], "80");
192        assert_eq!(emv["9F10"], "0102030405");
193    }
194
195    #[test]
196    fn test_secondary_bitmap_fields() {
197        let spec = default_spec();
198
199        let message = json!({
200            "mti": "0200",
201            "fields": {
202                "de003_processing_code": "000000",
203                "de070_network_management_information_code": "301"
204            }
205        });
206
207        let hex_encoded = publish(&message, &spec).expect("publish failed");
208        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
209
210        assert_eq!(parsed["mti"], "0200");
211        assert_eq!(
212            parsed["fields"]["de070_network_management_information_code"],
213            "301"
214        );
215    }
216
217    #[test]
218    fn test_binary_field_round_trip() {
219        let spec = default_spec();
220
221        let message = json!({
222            "mti": "0200",
223            "fields": {
224                "de003_processing_code": "000000",
225                "de052_pin_data": "1234567890ABCDEF"
226            }
227        });
228
229        let hex_encoded = publish(&message, &spec).expect("publish failed");
230        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
231
232        assert_eq!(parsed["fields"]["de052_pin_data"], "1234567890ABCDEF");
233    }
234
235    #[test]
236    fn test_parse_invalid_hex() {
237        let spec = default_spec();
238        let result = parse("ZZZZ", &spec);
239        assert!(result.is_err());
240    }
241
242    #[test]
243    fn test_parse_too_short() {
244        let spec = default_spec();
245        // Only 2 hex chars = 1 byte, not enough for MTI
246        let result = parse("30", &spec);
247        assert!(result.is_err());
248    }
249
250    // -----------------------------------------------------------------------
251    // Network-specific spec loading tests
252    // -----------------------------------------------------------------------
253
254    fn visa_spec() -> LoadedSpec {
255        let spec_json = include_str!("../specifications/visa_base1.json");
256        load_spec(spec_json).expect("failed to load Visa BASE I spec")
257    }
258
259    fn mastercard_spec() -> LoadedSpec {
260        let spec_json = include_str!("../specifications/mastercard_mip.json");
261        load_spec(spec_json).expect("failed to load Mastercard MIP spec")
262    }
263
264    #[test]
265    fn test_load_visa_spec() {
266        let spec = visa_spec();
267        assert_eq!(spec.version, "2003");
268        assert_eq!(spec.defaults.encoding, "ebcdic");
269        assert_eq!(spec.defaults.prefix_encoding, "bcd");
270        assert_eq!(spec.defaults.bitmap_encoding, "binary");
271        assert_eq!(spec.defaults.mti_encoding, "bcd");
272        assert_eq!(spec.defaults.max_bitmaps, 3);
273        assert!(spec.get_field(2).is_some());
274        assert!(spec.get_field(48).is_some());
275        assert!(spec.get_field(62).is_some());
276        assert!(spec.get_field(128).is_some());
277
278        // Verify DE 48 is a bitmap composite with proper config
279        let de48 = spec.get_field(48).unwrap();
280        assert_eq!(de48.field_type, field_spec::FieldType::Composite);
281        assert_eq!(de48.composite_type, Some(field_spec::CompositeType::Bitmap));
282        assert!(de48.bitmap.is_some());
283        let bm = de48.bitmap.as_ref().unwrap();
284        assert_eq!(bm.length, 4);
285        assert_eq!(bm.encoding, "binary");
286    }
287
288    #[test]
289    fn test_load_mastercard_spec() {
290        let spec = mastercard_spec();
291        assert_eq!(spec.version, "2003");
292        assert_eq!(spec.defaults.encoding, "ebcdic");
293        assert_eq!(spec.defaults.prefix_encoding, "bcd");
294        assert_eq!(spec.defaults.bitmap_encoding, "binary");
295        assert_eq!(spec.defaults.mti_encoding, "ascii");
296        assert_eq!(spec.defaults.max_bitmaps, 2);
297        assert!(spec.get_field(2).is_some());
298        assert!(spec.get_field(61).is_some());
299        assert!(spec.get_field(128).is_some());
300
301        // Verify DE 61 is an ordered composite
302        let de61 = spec.get_field(61).unwrap();
303        assert_eq!(de61.field_type, field_spec::FieldType::Composite);
304        assert_eq!(
305            de61.composite_type,
306            Some(field_spec::CompositeType::Ordered)
307        );
308        assert!(de61.subfields.is_some());
309        assert_eq!(de61.subfields.as_ref().unwrap().len(), 12);
310    }
311
312    #[test]
313    fn test_visa_bcd_mti_round_trip() {
314        let spec = visa_spec();
315
316        let message = json!({
317            "mti": "0100",
318            "fields": {
319                "de003_processing_code": "000000",
320                "de004_amount_transaction": "000000001000",
321                "de011_system_trace_audit_number": "123456",
322                "de025_pos_condition_code": "00"
323            }
324        });
325
326        let hex_encoded = publish(&message, &spec).expect("visa publish failed");
327        let parsed = parse(&hex_encoded, &spec).expect("visa parse failed");
328
329        assert_eq!(parsed["mti"], "0100");
330        assert_eq!(parsed["fields"]["de003_processing_code"], "000000");
331        assert_eq!(parsed["fields"]["de004_amount_transaction"], "000000001000");
332        assert_eq!(
333            parsed["fields"]["de011_system_trace_audit_number"],
334            "123456"
335        );
336        assert_eq!(parsed["fields"]["de025_pos_condition_code"], "00");
337    }
338
339    #[test]
340    fn test_visa_ebcdic_string_fields() {
341        let spec = visa_spec();
342
343        let message = json!({
344            "mti": "0100",
345            "fields": {
346                "de003_processing_code": "000000",
347                "de037_retrieval_reference_number": "425612000001",
348                "de038_authorization_id_response": "ABC123",
349                "de039_response_code": "00",
350                "de041_card_acceptor_terminal_id": "TERM0001"
351            }
352        });
353
354        let hex_encoded = publish(&message, &spec).expect("visa publish failed");
355        let parsed = parse(&hex_encoded, &spec).expect("visa parse failed");
356
357        assert_eq!(
358            parsed["fields"]["de037_retrieval_reference_number"],
359            "425612000001"
360        );
361        assert_eq!(
362            parsed["fields"]["de038_authorization_id_response"],
363            "ABC123"
364        );
365        assert_eq!(parsed["fields"]["de039_response_code"], "00");
366        assert_eq!(
367            parsed["fields"]["de041_card_acceptor_terminal_id"],
368            "TERM0001"
369        );
370    }
371
372    #[test]
373    fn test_visa_llvar_bcd_prefix() {
374        let spec = visa_spec();
375
376        let message = json!({
377            "mti": "0100",
378            "fields": {
379                "de002_primary_account_number": "4111111111111111",
380                "de003_processing_code": "000000"
381            }
382        });
383
384        let hex_encoded = publish(&message, &spec).expect("visa publish failed");
385        let parsed = parse(&hex_encoded, &spec).expect("visa parse failed");
386
387        assert_eq!(
388            parsed["fields"]["de002_primary_account_number"],
389            "4111111111111111"
390        );
391    }
392
393    #[test]
394    fn test_visa_bitmap_composite_round_trip() {
395        let spec = visa_spec();
396
397        let message = json!({
398            "mti": "0100",
399            "fields": {
400                "de003_processing_code": "000000",
401                "de062_custom_payment_service": {
402                    "authorization_characteristics": "A",
403                    "transaction_id": "123456789012345"
404                }
405            }
406        });
407
408        let hex_encoded = publish(&message, &spec).expect("visa publish failed");
409        let parsed = parse(&hex_encoded, &spec).expect("visa parse failed");
410
411        let de62 = &parsed["fields"]["de062_custom_payment_service"];
412        assert_eq!(de62["authorization_characteristics"], "A");
413        assert_eq!(de62["transaction_id"], "123456789012345");
414    }
415
416    #[test]
417    fn test_mastercard_ascii_mti_round_trip() {
418        let spec = mastercard_spec();
419
420        let message = json!({
421            "mti": "0100",
422            "fields": {
423                "de003_processing_code": "000000",
424                "de004_amount_transaction": "000000001000",
425                "de011_system_trace_audit_number": "123456",
426                "de025_pos_condition_code": "00"
427            }
428        });
429
430        let hex_encoded = publish(&message, &spec).expect("mc publish failed");
431        let parsed = parse(&hex_encoded, &spec).expect("mc parse failed");
432
433        assert_eq!(parsed["mti"], "0100");
434        assert_eq!(parsed["fields"]["de003_processing_code"], "000000");
435        assert_eq!(parsed["fields"]["de004_amount_transaction"], "000000001000");
436    }
437
438    #[test]
439    fn test_mastercard_ordered_composite_de61() {
440        let spec = mastercard_spec();
441
442        let message = json!({
443            "mti": "0100",
444            "fields": {
445                "de003_processing_code": "000000",
446                "de061_pos_data": {
447                    "pos_terminal_attendance": "1",
448                    "reserved": "0",
449                    "pos_terminal_location": "0",
450                    "pos_cardholder_presence": "0",
451                    "pos_card_presence": "0",
452                    "pos_card_capture": "0",
453                    "pos_transaction_status": "0",
454                    "pos_transaction_security": "0",
455                    "reserved2": "0",
456                    "cardholder_activated_terminal": "2",
457                    "pos_card_data_terminal_input": "1",
458                    "pos_authorization_life_cycle": "00"
459                }
460            }
461        });
462
463        let hex_encoded = publish(&message, &spec).expect("mc publish failed");
464        let parsed = parse(&hex_encoded, &spec).expect("mc parse failed");
465
466        let de61 = &parsed["fields"]["de061_pos_data"];
467        assert_eq!(de61["pos_terminal_attendance"], "1");
468        assert_eq!(de61["cardholder_activated_terminal"], "2");
469        assert_eq!(de61["pos_card_data_terminal_input"], "1");
470    }
471
472    #[test]
473    fn test_mastercard_ebcdic_string_fields() {
474        let spec = mastercard_spec();
475
476        let message = json!({
477            "mti": "0100",
478            "fields": {
479                "de003_processing_code": "000000",
480                "de037_retrieval_reference_number": "425612000001",
481                "de041_card_acceptor_terminal_id": "TERM0001",
482                "de042_card_acceptor_id_code": "MERCHANT000001 "
483            }
484        });
485
486        let hex_encoded = publish(&message, &spec).expect("mc publish failed");
487        let parsed = parse(&hex_encoded, &spec).expect("mc parse failed");
488
489        assert_eq!(
490            parsed["fields"]["de037_retrieval_reference_number"],
491            "425612000001"
492        );
493        assert_eq!(
494            parsed["fields"]["de041_card_acceptor_terminal_id"],
495            "TERM0001"
496        );
497    }
498
499    #[test]
500    fn test_visa_full_auth_round_trip() {
501        let spec = visa_spec();
502
503        let message = json!({
504            "mti": "0100",
505            "fields": {
506                "de002_primary_account_number": "4111111111111111",
507                "de003_processing_code": "000000",
508                "de004_amount_transaction": "000000005000",
509                "de007_transmission_date_time": "0101120000",
510                "de011_system_trace_audit_number": "000001",
511                "de014_expiration_date": "2512",
512                "de022_pos_entry_mode": {
513                    "pan_entry_mode": "05",
514                    "pin_entry_capability": "1"
515                },
516                "de025_pos_condition_code": "00",
517                "de037_retrieval_reference_number": "000000000001",
518                "de041_card_acceptor_terminal_id": "TERM0001",
519                "de042_card_acceptor_id_code": "MERCHANT000001 ",
520                "de049_currency_code_transaction": "840"
521            }
522        });
523
524        let hex_encoded = publish(&message, &spec).expect("visa full auth publish failed");
525        let parsed = parse(&hex_encoded, &spec).expect("visa full auth parse failed");
526
527        assert_eq!(parsed["mti"], "0100");
528        assert_eq!(
529            parsed["fields"]["de002_primary_account_number"],
530            "4111111111111111"
531        );
532        assert_eq!(parsed["fields"]["de004_amount_transaction"], "000000005000");
533        assert_eq!(parsed["fields"]["de049_currency_code_transaction"], "840");
534        let pos = &parsed["fields"]["de022_pos_entry_mode"];
535        assert_eq!(pos["pan_entry_mode"], "05");
536        assert_eq!(pos["pin_entry_capability"], "1");
537    }
538
539    #[test]
540    fn test_mastercard_full_auth_round_trip() {
541        let spec = mastercard_spec();
542
543        let message = json!({
544            "mti": "0100",
545            "fields": {
546                "de002_primary_account_number": "5500000000000004",
547                "de003_processing_code": "000000",
548                "de004_amount_transaction": "000000002500",
549                "de007_transmission_date_time": "0315093000",
550                "de011_system_trace_audit_number": "000042",
551                "de014_expiration_date": "2612",
552                "de022_pos_entry_mode": {
553                    "pan_entry_mode": "05",
554                    "pin_entry_capability": "1"
555                },
556                "de025_pos_condition_code": "00",
557                "de037_retrieval_reference_number": "000000000042",
558                "de041_card_acceptor_terminal_id": "TERM0002",
559                "de042_card_acceptor_id_code": "MC_MERCHANT001 ",
560                "de049_currency_code_transaction": "840"
561            }
562        });
563
564        let hex_encoded = publish(&message, &spec).expect("mc full auth publish failed");
565        let parsed = parse(&hex_encoded, &spec).expect("mc full auth parse failed");
566
567        assert_eq!(parsed["mti"], "0100");
568        assert_eq!(
569            parsed["fields"]["de002_primary_account_number"],
570            "5500000000000004"
571        );
572        assert_eq!(parsed["fields"]["de004_amount_transaction"], "000000002500");
573        assert_eq!(parsed["fields"]["de049_currency_code_transaction"], "840");
574    }
575
576    // -----------------------------------------------------------------------
577    // Original tests
578    // -----------------------------------------------------------------------
579
580    // -----------------------------------------------------------------------
581    // Validation tests
582    // -----------------------------------------------------------------------
583
584    #[test]
585    fn test_validate_valid_message() {
586        let spec = default_spec();
587        let message = json!({
588            "mti": "0100",
589            "fields": {
590                "de002_primary_account_number": "4111111111111111",
591                "de003_processing_code": "000000",
592                "de004_amount_transaction": "000000005000",
593                "de007_transmission_date_time": "0725143052",
594                "de011_system_trace_audit_number": "000001",
595                "de014_expiration_date": "2512",
596                "de022_pos_entry_mode": { "pan_entry_mode": "05", "pin_entry_capability": "1" },
597                "de025_pos_condition_code": "00",
598                "de041_card_acceptor_terminal_id": "TERM0001",
599                "de042_card_acceptor_id_code": "MERCHANT000001 ",
600                "de049_currency_code_transaction": "840"
601            }
602        });
603        let result = validate(&message, &spec);
604        assert!(
605            result.is_valid(),
606            "expected valid but got errors: {:?}",
607            result.errors
608        );
609    }
610
611    #[test]
612    fn test_validate_missing_mandatory_fields() {
613        let spec = default_spec();
614        let message = json!({
615            "mti": "0100",
616            "fields": {
617                "de003_processing_code": "000000",
618                "de007_transmission_date_time": "0725143052",
619                "de011_system_trace_audit_number": "000001",
620                "de014_expiration_date": "2512",
621                "de022_pos_entry_mode": { "pan_entry_mode": "05", "pin_entry_capability": "1" },
622                "de025_pos_condition_code": "00",
623                "de041_card_acceptor_terminal_id": "TERM0001",
624                "de042_card_acceptor_id_code": "MERCHANT000001 ",
625                "de049_currency_code_transaction": "840"
626            }
627        });
628        let result = validate(&message, &spec);
629        let mandatory_errors: Vec<_> = result
630            .errors
631            .iter()
632            .filter(|e| e.rule == "mandatory")
633            .collect();
634        assert_eq!(mandatory_errors.len(), 2); // missing DE 2 and DE 4
635        assert!(mandatory_errors.iter().any(|e| e.de == 2));
636        assert!(mandatory_errors.iter().any(|e| e.de == 4));
637    }
638
639    #[test]
640    fn test_validate_numeric_format_error() {
641        let spec = default_spec();
642        let message = json!({
643            "mti": "0200",
644            "fields": {
645                "de002_primary_account_number": "4111111111111111",
646                "de003_processing_code": "000000",
647                "de004_amount_transaction": "00ABC0001000",
648                "de007_transmission_date_time": "0725143052",
649                "de011_system_trace_audit_number": "123456",
650                "de022_pos_entry_mode": { "pan_entry_mode": "05", "pin_entry_capability": "1" },
651                "de025_pos_condition_code": "00",
652                "de041_card_acceptor_terminal_id": "TERM0001",
653                "de042_card_acceptor_id_code": "MERCHANT000001 ",
654                "de049_currency_code_transaction": "840"
655            }
656        });
657        let result = validate(&message, &spec);
658        let format_errors: Vec<_> = result
659            .errors
660            .iter()
661            .filter(|e| e.rule == "format" && e.de == 4)
662            .collect();
663        assert_eq!(format_errors.len(), 1);
664    }
665
666    #[test]
667    fn test_validate_length_exceeded() {
668        let spec = default_spec();
669        let message = json!({
670            "mti": "0200",
671            "fields": {
672                "de002_primary_account_number": "41111111111111111111",
673                "de003_processing_code": "000000",
674                "de004_amount_transaction": "000000001000",
675                "de007_transmission_date_time": "0725143052",
676                "de011_system_trace_audit_number": "123456",
677                "de022_pos_entry_mode": { "pan_entry_mode": "05", "pin_entry_capability": "1" },
678                "de025_pos_condition_code": "00",
679                "de041_card_acceptor_terminal_id": "TERM0001",
680                "de042_card_acceptor_id_code": "MERCHANT000001 ",
681                "de049_currency_code_transaction": "840"
682            }
683        });
684        let result = validate(&message, &spec);
685        let len_errors: Vec<_> = result
686            .errors
687            .iter()
688            .filter(|e| e.rule == "length" && e.de == 2)
689            .collect();
690        assert_eq!(len_errors.len(), 1);
691    }
692
693    #[test]
694    fn test_validate_fixed_length_mismatch() {
695        let spec = default_spec();
696        let message = json!({
697            "mti": "0200",
698            "fields": {
699                "de003_processing_code": "00"
700            }
701        });
702        let result = validate(&message, &spec);
703        let len_errors: Vec<_> = result
704            .errors
705            .iter()
706            .filter(|e| e.rule == "length" && e.de == 3)
707            .collect();
708        assert_eq!(len_errors.len(), 1);
709    }
710
711    #[test]
712    fn test_validate_date_pattern_invalid() {
713        let spec = default_spec();
714        let message = json!({
715            "mti": "0200",
716            "fields": {
717                "de007_transmission_date_time": "1325143052"
718            }
719        });
720        let result = validate(&message, &spec);
721        let pattern_errors: Vec<_> = result
722            .errors
723            .iter()
724            .filter(|e| e.rule == "pattern" && e.de == 7)
725            .collect();
726        assert_eq!(pattern_errors.len(), 1);
727    }
728
729    #[test]
730    fn test_validate_date_pattern_valid() {
731        let spec = default_spec();
732        let message = json!({
733            "mti": "0200",
734            "fields": {
735                "de007_transmission_date_time": "0725143052"
736            }
737        });
738        let result = validate(&message, &spec);
739        let pattern_errors: Vec<_> = result
740            .errors
741            .iter()
742            .filter(|e| e.rule == "pattern" && e.de == 7)
743            .collect();
744        assert_eq!(pattern_errors.len(), 0);
745    }
746
747    #[test]
748    fn test_validate_response_code_valid() {
749        let spec = default_spec();
750        let message = json!({
751            "mti": "0210",
752            "fields": {
753                "de003_processing_code": "000000",
754                "de004_amount_transaction": "000000001000",
755                "de007_transmission_date_time": "0725143052",
756                "de011_system_trace_audit_number": "123456",
757                "de039_response_code": "00"
758            }
759        });
760        let result = validate(&message, &spec);
761        let rc_errors: Vec<_> = result
762            .errors
763            .iter()
764            .filter(|e| e.rule == "response_code")
765            .collect();
766        assert_eq!(rc_errors.len(), 0);
767    }
768
769    #[test]
770    fn test_validate_response_code_unknown() {
771        let spec = default_spec();
772        let message = json!({
773            "mti": "0210",
774            "fields": {
775                "de039_response_code": "ZZ"
776            }
777        });
778        let result = validate(&message, &spec);
779        let rc_errors: Vec<_> = result
780            .errors
781            .iter()
782            .filter(|e| e.rule == "response_code")
783            .collect();
784        assert_eq!(rc_errors.len(), 1);
785    }
786
787    #[test]
788    fn test_validate_invalid_mti() {
789        let spec = default_spec();
790        let message = json!({
791            "mti": "0090",
792            "fields": {}
793        });
794        let result = validate(&message, &spec);
795        let mti_errors: Vec<_> = result.errors.iter().filter(|e| e.rule == "mti").collect();
796        assert!(!mti_errors.is_empty());
797    }
798
799    #[test]
800    fn test_validate_missing_mti() {
801        let spec = default_spec();
802        let message = json!({
803            "fields": {}
804        });
805        let result = validate(&message, &spec);
806        assert!(!result.is_valid());
807        assert_eq!(result.errors[0].rule, "mti");
808    }
809
810    #[test]
811    fn test_validate_multiple_errors() {
812        let spec = default_spec();
813        let message = json!({
814            "mti": "0200",
815            "fields": {
816                "de003_processing_code": "00",
817                "de004_amount_transaction": "00ABC0001000",
818                "de007_transmission_date_time": "1325143052"
819            }
820        });
821        let result = validate(&message, &spec);
822        assert!(result.errors.len() >= 3);
823    }
824
825    #[test]
826    fn test_validate_composite_skipped() {
827        let spec = default_spec();
828        let message = json!({
829            "mti": "0200",
830            "fields": {
831                "de022_pos_entry_mode": { "pan_entry_mode": "05", "pin_entry_capability": "1" }
832            }
833        });
834        let result = validate(&message, &spec);
835        let de22_errors: Vec<_> = result.errors.iter().filter(|e| e.de == 22).collect();
836        assert_eq!(de22_errors.len(), 0);
837    }
838
839    #[test]
840    fn test_lookup_response_code() {
841        assert_eq!(lookup_response_code("00"), Some("Approved"));
842        assert_eq!(lookup_response_code("51"), Some("Insufficient Funds"));
843        assert_eq!(lookup_response_code("ZZ"), None);
844    }
845
846    #[test]
847    fn test_validate_no_mti_rules() {
848        let spec = default_spec();
849        let message = json!({
850            "mti": "0300",
851            "fields": {
852                "de003_processing_code": "000000"
853            }
854        });
855        let result = validate(&message, &spec);
856        let mandatory_errors: Vec<_> = result
857            .errors
858            .iter()
859            .filter(|e| e.rule == "mandatory")
860            .collect();
861        assert_eq!(mandatory_errors.len(), 0);
862    }
863
864    #[test]
865    fn test_validate_yymm_pattern() {
866        let spec = default_spec();
867        let message = json!({
868            "mti": "0200",
869            "fields": {
870                "de014_expiration_date": "2513"
871            }
872        });
873        let result = validate(&message, &spec);
874        let pattern_errors: Vec<_> = result
875            .errors
876            .iter()
877            .filter(|e| e.rule == "pattern" && e.de == 14)
878            .collect();
879        assert_eq!(pattern_errors.len(), 1);
880    }
881
882    // -----------------------------------------------------------------------
883    // Original tests (continued)
884    // -----------------------------------------------------------------------
885
886    // -----------------------------------------------------------------------
887    // Phase 1: Bitmap composite unknown subfield handling
888    // -----------------------------------------------------------------------
889
890    #[test]
891    fn test_bitmap_composite_unknown_subfield_with_default() {
892        let spec = visa_spec();
893        // DE 48 now has default_subfield configured, so unknown bits should work
894        let message = json!({
895            "mti": "0100",
896            "fields": {
897                "de003_processing_code": "000000",
898                "de048_additional_data_private": {
899                    "additional_pos_data": "12345678901234567890123456",
900                    "merchant_category_override": "5411"
901                }
902            }
903        });
904
905        let hex_encoded = publish(&message, &spec).expect("visa publish failed");
906        let parsed = parse(&hex_encoded, &spec).expect("visa parse failed");
907
908        let de48 = &parsed["fields"]["de048_additional_data_private"];
909        assert_eq!(de48["additional_pos_data"], "12345678901234567890123456");
910        assert_eq!(de48["merchant_category_override"], "5411");
911    }
912
913    // -----------------------------------------------------------------------
914    // Phase 2: Expanded Visa subfields
915    // -----------------------------------------------------------------------
916
917    #[test]
918    fn test_visa_expanded_de62_subfields() {
919        let spec = visa_spec();
920
921        let message = json!({
922            "mti": "0100",
923            "fields": {
924                "de003_processing_code": "000000",
925                "de062_custom_payment_service": {
926                    "authorization_characteristics": "A",
927                    "transaction_id": "123456789012345",
928                    "validation_code": "ABCD",
929                    "market_specific_data": "H"
930                }
931            }
932        });
933
934        let hex_encoded = publish(&message, &spec).expect("visa publish failed");
935        let parsed = parse(&hex_encoded, &spec).expect("visa parse failed");
936
937        let de62 = &parsed["fields"]["de062_custom_payment_service"];
938        assert_eq!(de62["authorization_characteristics"], "A");
939        assert_eq!(de62["transaction_id"], "123456789012345");
940        assert_eq!(de62["validation_code"], "ABCD");
941        assert_eq!(de62["market_specific_data"], "H");
942    }
943
944    // -----------------------------------------------------------------------
945    // Phase 3: x+n field type
946    // -----------------------------------------------------------------------
947
948    #[test]
949    fn test_xn_field_round_trip() {
950        let spec = default_spec();
951
952        let message = json!({
953            "mti": "0200",
954            "fields": {
955                "de003_processing_code": "000000",
956                "de028_amount_transaction_fee": "C00000150"
957            }
958        });
959
960        let hex_encoded = publish(&message, &spec).expect("publish failed");
961        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
962
963        assert_eq!(
964            parsed["fields"]["de028_amount_transaction_fee"],
965            "C00000150"
966        );
967    }
968
969    #[test]
970    fn test_xn_field_debit_round_trip() {
971        let spec = default_spec();
972
973        let message = json!({
974            "mti": "0200",
975            "fields": {
976                "de003_processing_code": "000000",
977                "de028_amount_transaction_fee": "D00000200"
978            }
979        });
980
981        let hex_encoded = publish(&message, &spec).expect("publish failed");
982        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
983
984        assert_eq!(
985            parsed["fields"]["de028_amount_transaction_fee"],
986            "D00000200"
987        );
988    }
989
990    #[test]
991    fn test_validate_xn_format() {
992        let spec = default_spec();
993
994        // Valid x+n
995        let message = json!({
996            "mti": "0200",
997            "fields": {
998                "de028_amount_transaction_fee": "C00000150"
999            }
1000        });
1001        let result = validate(&message, &spec);
1002        let xn_errors: Vec<_> = result
1003            .errors
1004            .iter()
1005            .filter(|e| e.de == 28 && e.rule == "format")
1006            .collect();
1007        assert_eq!(xn_errors.len(), 0);
1008
1009        // Invalid x+n (starts with X instead of C/D)
1010        let message_bad = json!({
1011            "mti": "0200",
1012            "fields": {
1013                "de028_amount_transaction_fee": "X00000150"
1014            }
1015        });
1016        let result_bad = validate(&message_bad, &spec);
1017        let xn_errors_bad: Vec<_> = result_bad
1018            .errors
1019            .iter()
1020            .filter(|e| e.de == 28 && e.rule == "format")
1021            .collect();
1022        assert!(xn_errors_bad.len() >= 1);
1023    }
1024
1025    // -----------------------------------------------------------------------
1026    // Phase 4: MTI version 9
1027    // -----------------------------------------------------------------------
1028
1029    #[test]
1030    fn test_mti_version_9() {
1031        let spec = default_spec();
1032
1033        let message = json!({
1034            "mti": "9100",
1035            "fields": {
1036                "de003_processing_code": "000000"
1037            }
1038        });
1039
1040        let hex_encoded = publish(&message, &spec).expect("publish failed");
1041        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
1042        assert_eq!(parsed["mti"], "9100");
1043
1044        // Validate should accept version 9
1045        let result = validate(&message, &spec);
1046        let mti_errors: Vec<_> = result.errors.iter().filter(|e| e.rule == "mti").collect();
1047        assert_eq!(mti_errors.len(), 0);
1048    }
1049
1050    #[test]
1051    fn test_classify_mti_version_9() {
1052        let c = mti::classify_mti("9100").unwrap();
1053        assert_eq!(c.version, "Private/Test");
1054        assert_eq!(c.message_class, "Authorization");
1055        assert_eq!(c.function, "Request");
1056    }
1057
1058    // -----------------------------------------------------------------------
1059    // Phase 5: TLV 3-byte tags, PDS composite
1060    // -----------------------------------------------------------------------
1061
1062    #[test]
1063    fn test_tlv_3byte_tag_round_trip() {
1064        let spec = default_spec();
1065
1066        // Use a 3-byte tag: 9F8101 (second byte has bit 8 set)
1067        let message = json!({
1068            "mti": "0200",
1069            "fields": {
1070                "de003_processing_code": "000000",
1071                "de055_emv_data": {
1072                    "9F26": "AABBCCDD11223344",
1073                    "9F8101": "FF"
1074                }
1075            }
1076        });
1077
1078        let hex_encoded = publish(&message, &spec).expect("publish failed");
1079        let parsed = parse(&hex_encoded, &spec).expect("parse failed");
1080
1081        let emv = &parsed["fields"]["de055_emv_data"];
1082        assert_eq!(emv["9F26"], "AABBCCDD11223344");
1083        assert_eq!(emv["9F8101"], "FF");
1084    }
1085
1086    #[test]
1087    fn test_mastercard_pds_round_trip() {
1088        let spec = mastercard_spec();
1089
1090        let message = json!({
1091            "mti": "0100",
1092            "fields": {
1093                "de003_processing_code": "000000",
1094                "de048_additional_data_private": {
1095                    "0043": "12345",
1096                    "0023": "ABCDE"
1097                }
1098            }
1099        });
1100
1101        let hex_encoded = publish(&message, &spec).expect("mc pds publish failed");
1102        let parsed = parse(&hex_encoded, &spec).expect("mc pds parse failed");
1103
1104        let de48 = &parsed["fields"]["de048_additional_data_private"];
1105        assert_eq!(de48["0023"], "ABCDE");
1106        assert_eq!(de48["0043"], "12345");
1107    }
1108
1109    // -----------------------------------------------------------------------
1110    // Phase 6: Response codes, tertiary bitmap fields
1111    // -----------------------------------------------------------------------
1112
1113    #[test]
1114    fn test_new_response_codes() {
1115        assert_eq!(lookup_response_code("16"), Some("Approved, Update Track 3"));
1116        assert_eq!(lookup_response_code("17"), Some("Customer Cancellation"));
1117        assert_eq!(lookup_response_code("20"), Some("Invalid Response"));
1118        assert_eq!(lookup_response_code("22"), Some("Suspected Malfunction"));
1119        assert_eq!(
1120            lookup_response_code("35"),
1121            Some("Contact Acquirer, Pick Up")
1122        );
1123        assert_eq!(
1124            lookup_response_code("37"),
1125            Some("Call Acquirer Security, Pick Up")
1126        );
1127        assert_eq!(lookup_response_code("42"), Some("No Universal Account"));
1128        assert_eq!(lookup_response_code("50"), Some("Do Not Renew"));
1129        assert_eq!(lookup_response_code("60"), Some("Contact Acquirer"));
1130        assert_eq!(lookup_response_code("66"), Some("Acquirer Response Error"));
1131        assert_eq!(lookup_response_code("67"), Some("Hard Capture, Pick Up"));
1132        assert_eq!(lookup_response_code("90"), Some("Cutoff in Progress"));
1133        assert_eq!(lookup_response_code("99"), Some("Reserved for Private Use"));
1134    }
1135
1136    #[test]
1137    fn test_visa_tertiary_bitmap_field_round_trip() {
1138        let spec = visa_spec();
1139
1140        let message = json!({
1141            "mti": "0100",
1142            "fields": {
1143                "de003_processing_code": "000000",
1144                "de130_reserved_private_130": "TESTDATA"
1145            }
1146        });
1147
1148        let hex_encoded = publish(&message, &spec).expect("visa tertiary publish failed");
1149        let parsed = parse(&hex_encoded, &spec).expect("visa tertiary parse failed");
1150
1151        assert_eq!(parsed["fields"]["de130_reserved_private_130"], "TESTDATA");
1152    }
1153
1154    #[test]
1155    fn test_authorization_request_response_pattern() {
1156        let spec = default_spec();
1157
1158        // Authorization request 0100
1159        let request = json!({
1160            "mti": "0100",
1161            "fields": {
1162                "de002_primary_account_number": "4111111111111111",
1163                "de003_processing_code": "000000",
1164                "de004_amount_transaction": "000000005000",
1165                "de007_transmission_date_time": "0101120000",
1166                "de011_system_trace_audit_number": "000001",
1167                "de014_expiration_date": "2512",
1168                "de022_pos_entry_mode": {
1169                    "pan_entry_mode": "05",
1170                    "pin_entry_capability": "1"
1171                },
1172                "de025_pos_condition_code": "00",
1173                "de026_pos_capture_code": "12",
1174                "de037_retrieval_reference_number": "000000000001",
1175                "de041_card_acceptor_terminal_id": "TERM0001",
1176                "de042_card_acceptor_id_code": "MERCHANT000001 ",
1177                "de043_card_acceptor_name_location": "ACME STORE          NEW YORK     NY US  ",
1178                "de049_currency_code_transaction": "840"
1179            }
1180        });
1181
1182        let hex_req = publish(&request, &spec).expect("publish request failed");
1183        let parsed_req = parse(&hex_req, &spec).expect("parse request failed");
1184        assert_eq!(parsed_req["mti"], "0100");
1185        assert_eq!(
1186            parsed_req["fields"]["de002_primary_account_number"],
1187            "4111111111111111"
1188        );
1189    }
1190}