///|
/// Build a stable validation issue for message-pair correlation.
fn correlation_issue(
code : String,
field : Int,
message : String,
) -> ValidationIssue {
{ code, field, message, severity: "error", }
}
///|
/// Compare a field that must exist in both messages and remain unchanged.
fn require_echoed_field(
request : IsoMessage,
response : IsoMessage,
field : Int,
issues : Array[ValidationIssue],
) -> Unit {
match (request.field(field), response.field(field)) {
(None, _) =>
issues.push(
correlation_issue(
"CORRELATION_REQUEST_MISSING",
field,
"request is missing correlation field \{field}",
),
)
(Some(_), None) =>
issues.push(
correlation_issue(
"CORRELATION_RESPONSE_MISSING",
field,
"response is missing echoed field \{field}",
),
)
(Some(expected), Some(actual)) =>
if expected != actual {
issues.push(
correlation_issue(
"CORRELATION_MISMATCH",
field,
"field \{field} changed from \{expected} to \{actual}",
),
)
}
}
}
///|
/// Compare an optional request field only when the response echoes it.
fn compare_if_echoed(
request : IsoMessage,
response : IsoMessage,
field : Int,
issues : Array[ValidationIssue],
) -> Unit {
match (request.field(field), response.field(field)) {
(Some(expected), Some(actual)) =>
if expected != actual {
issues.push(
correlation_issue(
"CORRELATION_MISMATCH",
field,
"optional echoed field \{field} changed from \{expected} to \{actual}",
),
)
}
_ => ()
}
}
///|
/// Validate that a response belongs to a particular request.
pub fn validate_response_correlation(
request : IsoMessage,
response : IsoMessage,
) -> Array[ValidationIssue] {
let issues : Array[ValidationIssue] = []
let request_mti = match parse_mti(request.mti) {
Ok(value) => value
Err(_) => {
issues.push(
correlation_issue(
"CORRELATION_REQUEST_MTI",
0,
"request has invalid MTI \{request.mti}",
),
)
return issues
}
}
if !request_mti.is_request() {
issues.push(
correlation_issue(
"CORRELATION_NOT_REQUEST",
0,
"MTI \{request.mti} is not request-like",
),
)
}
match response_mti(request.mti) {
Ok(expected) =>
if response.mti != expected {
issues.push(
correlation_issue(
"CORRELATION_RESPONSE_MTI",
0,
"expected response MTI \{expected}, got \{response.mti}",
),
)
}
Err(_) =>
issues.push(
correlation_issue(
"CORRELATION_RESPONSE_MTI",
0,
"cannot derive a response MTI from \{request.mti}",
),
)
}
require_echoed_field(request, response, 11, issues)
match request_mti.message_class {
Authorization | Financial => {
require_echoed_field(request, response, 3, issues)
require_echoed_field(request, response, 4, issues)
require_echoed_field(request, response, 41, issues)
require_echoed_field(request, response, 49, issues)
compare_if_echoed(request, response, 2, issues)
compare_if_echoed(request, response, 32, issues)
compare_if_echoed(request, response, 37, issues)
compare_if_echoed(request, response, 42, issues)
}
Reversal => {
require_echoed_field(request, response, 3, issues)
require_echoed_field(request, response, 4, issues)
require_echoed_field(request, response, 41, issues)
require_echoed_field(request, response, 49, issues)
require_echoed_field(request, response, 90, issues)
compare_if_echoed(request, response, 37, issues)
}
NetworkManagement => {
require_echoed_field(request, response, 70, issues)
compare_if_echoed(request, response, 32, issues)
}
_ => ()
}
if !response.has_field(39) {
issues.push(
correlation_issue(
"CORRELATION_RESPONSE_CODE", 39, "response must contain field 39 response code",
),
)
}
if response.has_field(52) {
issues.push(
correlation_issue(
"CORRELATION_SENSITIVE_ECHO", 52, "response must not echo PIN data",
),
)
}
issues
}
///|
/// True when request and response pass all correlation checks.
pub fn response_correlates(request : IsoMessage, response : IsoMessage) -> Bool {
validate_response_correlation(request, response).length() == 0
}
///|
/// Copy a field from source when present.
fn copy_field_if_present(
source : IsoMessage,
target : IsoMessage,
field : Int,
) -> Result[Unit, IsoError] {
match source.field(field) {
Some(value) => target.set_field(field, value)
None => Ok(())
}
}
///|
/// Build a minimal response skeleton with correlation fields and response code.
pub fn response_skeleton(
request : IsoMessage,
response_code : String,
) -> Result[IsoMessage, IsoError] {
if response_code.length() != 2 {
return Err(TemplateViolation("response code must contain two digits"))
}
match validate_numeric_text(39, response_code) {
Err(_) =>
return Err(TemplateViolation("response code must contain two digits"))
Ok(_) => ()
}
let mti = match response_mti(request.mti) {
Ok(value) => value
Err(error) => return Err(error)
}
let response = match message(mti) {
Ok(value) => value
Err(error) => return Err(error)
}
let parsed = match parse_mti(request.mti) {
Ok(value) => value
Err(error) => return Err(error)
}
let common = [7, 11, 12, 13, 32, 37]
for field in common {
match copy_field_if_present(request, response, field) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
let class_fields = match parsed.message_class {
Authorization | Financial => [3, 4, 41, 42, 49]
Reversal => [3, 4, 41, 49, 90]
NetworkManagement => [70]
_ => []
}
for field in class_fields {
match copy_field_if_present(request, response, field) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
match response.set_field(39, response_code) {
Err(error) => return Err(error)
Ok(_) => ()
}
Ok(response)
}
///|
/// Validate that a reversal points to and preserves key data from its original transaction.
pub fn validate_reversal_correlation(
original : IsoMessage,
reversal : IsoMessage,
) -> Array[ValidationIssue] {
let issues : Array[ValidationIssue] = []
let reversal_mti_value = match parse_mti(reversal.mti) {
Ok(value) => value
Err(_) => {
issues.push(
correlation_issue(
"REVERSAL_MTI",
0,
"reversal has invalid MTI \{reversal.mti}",
),
)
return issues
}
}
if reversal_mti_value.message_class != Reversal {
issues.push(
correlation_issue(
"REVERSAL_MTI",
0,
"MTI \{reversal.mti} is not a reversal message",
),
)
}
match reversal.field(90) {
None =>
issues.push(
correlation_issue(
"REVERSAL_ORIGINAL_DATA", 90, "reversal is missing DE90 original data elements",
),
)
Some(value) =>
match parse_original_data(value) {
Err(error) =>
issues.push(
correlation_issue("REVERSAL_ORIGINAL_DATA", 90, error.message()),
)
Ok(data) =>
if !data.matches(original) {
issues.push(
correlation_issue(
"REVERSAL_ORIGINAL_MISMATCH", 90, "DE90 does not identify the supplied original message",
),
)
}
}
}
for field in [3, 4, 41, 49] {
require_echoed_field(original, reversal, field, issues)
}
compare_if_echoed(original, reversal, 2, issues)
compare_if_echoed(original, reversal, 32, issues)
compare_if_echoed(original, reversal, 37, issues)
compare_if_echoed(original, reversal, 42, issues)
if reversal.field(11) == original.field(11) {
issues.push(
correlation_issue(
"REVERSAL_STAN_REUSED", 11, "reversal should use a new STAN while DE90 retains the original STAN",
),
)
}
if reversal.has_field(52) {
issues.push(
correlation_issue(
"REVERSAL_PIN_COPIED", 52, "PIN data must not be copied into a reversal",
),
)
}
issues
}
///|
/// Build a reversal request skeleton with a new STAN and transmission time.
pub fn reversal_skeleton(
original : IsoMessage,
reversal_stan : String,
reversal_transmission_datetime : String,
) -> Result[IsoMessage, IsoError] {
if reversal_stan.length() != 6 {
return Err(TemplateViolation("reversal STAN must contain six digits"))
}
match validate_numeric_text(11, reversal_stan) {
Err(_) =>
return Err(TemplateViolation("reversal STAN must contain six digits"))
Ok(_) => ()
}
match validate_transmission_datetime(reversal_transmission_datetime) {
Err(error) => return Err(error)
Ok(_) => ()
}
let mti = match reversal_mti(original.mti) {
Ok(value) => value
Err(error) => return Err(error)
}
let reversal = match message(mti) {
Ok(value) => value
Err(error) => return Err(error)
}
let copied = [2, 3, 4, 12, 13, 18, 22, 25, 32, 35, 37, 41, 42, 49, 55, 60]
for field in copied {
match copy_field_if_present(original, reversal, field) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
match reversal.set_field(7, reversal_transmission_datetime) {
Err(error) => return Err(error)
Ok(_) => ()
}
match reversal.set_field(11, reversal_stan) {
Err(error) => return Err(error)
Ok(_) => ()
}
match attach_original_data(reversal, original) {
Err(error) => return Err(error)
Ok(_) => ()
}
Ok(reversal)
}