///|
/// Parsed DE7 transmission date and time (MMDDhhmmss).
pub(all) struct TransmissionDateTime {
month : Int
day : Int
hour : Int
minute : Int
second : Int
} derive(Eq, Debug)
///|
/// Parsed DE12 local time (hhmmss).
pub(all) struct LocalTransactionTime {
hour : Int
minute : Int
second : Int
} derive(Eq, Debug)
///|
/// Parsed DE13 local date (MMDD).
pub(all) struct LocalTransactionDate {
month : Int
day : Int
} derive(Eq, Debug)
///|
/// Parsed DE14 card expiration (YYMM).
pub(all) struct CardExpiration {
year : Int
month : Int
} derive(Eq, Debug)
///|
/// Parsed systems trace audit number.
pub(all) struct Stan {
text : String
number : Int
} derive(Eq, Debug)
///|
/// Parsed retrieval reference number.
pub(all) struct RetrievalReference {
text : String
} derive(Eq, Debug)
///|
/// Parsed response-code metadata.
pub(all) struct ResponseCodeInfo {
code : String
approved : Bool
category : String
meaning : String
} derive(Eq, Debug)
///|
/// Common network management operations carried in DE70.
pub(all) enum NetworkManagementOperation {
SignOn
SignOff
EchoTest
Cutover
KeyChange
UnknownNetworkOperation(String)
} derive(Eq, Debug)
///|
fn two_decimal(value : String, offset : Int) -> Int {
(value[offset].to_int() - 48) * 10 + value[offset + 1].to_int() - 48
}
///|
fn valid_month_day(month : Int, day : Int) -> Bool {
if month < 1 || month > 12 || day < 1 {
return false
}
let maximum = match month {
2 => 29
4 | 6 | 9 | 11 => 30
_ => 31
}
day <= maximum
}
///|
fn require_decimal_width(
field : Int,
value : String,
width : Int,
) -> Result[Unit, IsoError] {
if value.length() != width {
return Err(InvalidLength(field, width, value.length()))
}
match validate_numeric_text(field, value) {
Ok(_) => Ok(())
Err(_) => Err(InvalidNumeric(field, value))
}
}
///|
/// Parse DE7 with calendar and clock range checks.
pub fn parse_transmission_datetime(
value : String,
) -> Result[TransmissionDateTime, IsoError] {
match require_decimal_width(7, value, 10) {
Err(_) => return Err(InvalidDateTime(value))
Ok(_) => ()
}
let month = two_decimal(value, 0)
let day = two_decimal(value, 2)
let hour = two_decimal(value, 4)
let minute = two_decimal(value, 6)
let second = two_decimal(value, 8)
if !valid_month_day(month, day) || hour > 23 || minute > 59 || second > 59 {
return Err(InvalidDateTime(value))
}
Ok({ month, day, hour, minute, second, })
}
///|
/// Parse DE12 local transaction time.
pub fn parse_local_transaction_time(
value : String,
) -> Result[LocalTransactionTime, IsoError] {
match require_decimal_width(12, value, 6) {
Err(_) => return Err(InvalidDateTime(value))
Ok(_) => ()
}
let hour = two_decimal(value, 0)
let minute = two_decimal(value, 2)
let second = two_decimal(value, 4)
if hour > 23 || minute > 59 || second > 59 {
return Err(InvalidDateTime(value))
}
Ok({ hour, minute, second, })
}
///|
/// Parse DE13 local transaction date.
pub fn parse_local_transaction_date(
value : String,
) -> Result[LocalTransactionDate, IsoError] {
match require_decimal_width(13, value, 4) {
Err(_) => return Err(InvalidDateTime(value))
Ok(_) => ()
}
let month = two_decimal(value, 0)
let day = two_decimal(value, 2)
if !valid_month_day(month, day) {
return Err(InvalidDateTime(value))
}
Ok({ month, day, })
}
///|
/// Parse DE14 card expiration date.
pub fn parse_card_expiration(
value : String,
) -> Result[CardExpiration, IsoError] {
match require_decimal_width(14, value, 4) {
Err(_) => return Err(InvalidDateTime(value))
Ok(_) => ()
}
let year = two_decimal(value, 0)
let month = two_decimal(value, 2)
if month < 1 || month > 12 {
return Err(InvalidDateTime(value))
}
Ok({ year, month, })
}
///|
/// True after the supplied two-digit calendar month has passed.
pub fn CardExpiration::expired_at(
self : CardExpiration,
year : Int,
month : Int,
) -> Bool {
self.year < year || (self.year == year && self.month < month)
}
///|
/// Parse a six-digit STAN while preserving leading zeroes.
pub fn parse_stan(value : String) -> Result[Stan, IsoError] {
match require_decimal_width(11, value, 6) {
Err(error) => return Err(error)
Ok(_) => ()
}
let mut number = 0
for i = 0; i < 6; i = i + 1 {
number = number * 10 + value[i].to_int() - 48
}
Ok({ text: value, number, })
}
///|
/// Return the next STAN with six-digit wraparound.
pub fn Stan::next(self : Stan) -> Stan {
let next = (self.number + 1) % 1000000
{ text: decimal_width(next, 6).unwrap(), number: next, }
}
///|
/// Parse a 12-character retrieval reference number.
pub fn parse_retrieval_reference(
value : String,
) -> Result[RetrievalReference, IsoError] {
if value.length() != 12 {
return Err(InvalidLength(37, 12, value.length()))
}
match validate_alphanumeric_text(37, value, false) {
Err(error) => Err(error)
Ok(_) => Ok({ text: value, })
}
}
///|
/// Validate an eight-character terminal identifier.
pub fn validate_terminal_id(value : String) -> Result[Unit, IsoError] {
if value.length() != 8 {
return Err(InvalidLength(41, 8, value.length()))
}
match validate_printable_text(41, value) {
Err(error) => return Err(error)
Ok(_) => ()
}
if value.trim().length() == 0 {
return Err(InvalidCharacter(41, 0, "blank terminal identifier"))
}
Ok(())
}
///|
/// Interpret widely used ISO 8583 response codes without imposing one network dialect.
pub fn parse_response_code(
value : String,
) -> Result[ResponseCodeInfo, IsoError] {
match require_decimal_width(39, value, 2) {
Err(error) => return Err(error)
Ok(_) => ()
}
let info = match value {
"00" => (true, "approved", "approved or completed successfully")
"01" => (false, "referral", "refer to card issuer")
"03" => (false, "configuration", "invalid merchant or acceptor")
"05" => (false, "decline", "do not honor")
"12" => (false, "format", "invalid transaction")
"13" => (false, "format", "invalid amount")
"14" => (false, "account", "invalid account number")
"30" => (false, "format", "format error")
"41" => (false, "card", "lost card")
"43" => (false, "card", "stolen card")
"51" => (false, "funds", "insufficient funds")
"54" => (false, "card", "expired card")
"55" => (false, "authentication", "incorrect PIN")
"57" => (false, "restriction", "transaction not permitted to cardholder")
"58" => (false, "restriction", "transaction not permitted to terminal")
"61" => (false, "limit", "amount limit exceeded")
"68" => (false, "timeout", "response received too late")
"75" => (false, "authentication", "PIN tries exceeded")
"91" => (false, "availability", "issuer or switch unavailable")
"94" => (false, "duplicate", "duplicate transmission")
"96" => (false, "system", "system malfunction")
_ => (false, "network-defined", "network-specific response code")
}
Ok({ code: value, approved: info.0, category: info.1, meaning: info.2, })
}
///|
/// Parse common DE70 operation codes.
pub fn parse_network_management_code(
value : String,
) -> Result[NetworkManagementOperation, IsoError] {
match require_decimal_width(70, value, 3) {
Err(error) => return Err(error)
Ok(_) => ()
}
Ok(
match value {
"001" => SignOn
"002" => SignOff
"101" => KeyChange
"201" => Cutover
"301" => EchoTest
other => UnknownNetworkOperation(other)
},
)
}
///|
fn domain_issue(
field : Int,
code : String,
message : String,
) -> ValidationIssue {
{ code, field, message, severity: "error", }
}
///|
/// Validate common field domains and cross-field consistency.
pub fn validate_common_domains(message : IsoMessage) -> Array[ValidationIssue] {
let issues : Array[ValidationIssue] = []
let checks : Array[(Int, (String) -> Result[Unit, IsoError])] = [
(7, fn(value) { parse_transmission_datetime(value).map(fn(_) { () }) }),
(11, fn(value) { parse_stan(value).map(fn(_) { () }) }),
(12, fn(value) { parse_local_transaction_time(value).map(fn(_) { () }) }),
(13, fn(value) { parse_local_transaction_date(value).map(fn(_) { () }) }),
(14, fn(value) { parse_card_expiration(value).map(fn(_) { () }) }),
(37, fn(value) { parse_retrieval_reference(value).map(fn(_) { () }) }),
(39, fn(value) { parse_response_code(value).map(fn(_) { () }) }),
(41, validate_terminal_id),
(70, fn(value) { parse_network_management_code(value).map(fn(_) { () }) }),
]
for check in checks {
match message.field(check.0) {
None => ()
Some(value) =>
match (check.1)(value) {
Ok(_) => ()
Err(error) =>
issues.push(domain_issue(check.0, error.code(), error.message()))
}
}
}
match (message.field(2), message.field(35)) {
(Some(pan), Some(track_text)) =>
match parse_track2(track_text) {
Ok(track) =>
if track.pan != pan {
issues.push(
domain_issue(
35, "PAN_TRACK_MISMATCH", "field 2 PAN and field 35 Track 2 PAN differ",
),
)
}
Err(_) => ()
}
_ => ()
}
match (message.field(14), message.field(35)) {
(Some(expiry), Some(track_text)) =>
match parse_track2(track_text) {
Ok(track) =>
if track.expiration != expiry {
issues.push(
domain_issue(
14, "EXPIRY_TRACK_MISMATCH", "field 14 expiry and field 35 Track 2 expiry differ",
),
)
}
Err(_) => ()
}
_ => ()
}
if message.has_field(12) != message.has_field(13) {
issues.push(
domain_issue(
12, "LOCAL_DATETIME_PAIR", "fields 12 local time and 13 local date should be supplied together",
),
)
}
let parsed_mti = parse_mti(message.mti)
match parsed_mti {
Ok(mti) => {
if mti.message_class == NetworkManagement && !message.has_field(70) {
issues.push(
domain_issue(
70, "NETWORK_CODE_REQUIRED", "network management message requires field 70",
),
)
}
if mti.message_class != NetworkManagement && message.has_field(70) {
issues.push(
domain_issue(
70, "NETWORK_CODE_UNEXPECTED", "field 70 is reserved for network management traffic",
),
)
}
if mti.message_class == Reversal && !message.has_field(90) {
issues.push(
domain_issue(
90, "ORIGINAL_DATA_REQUIRED", "reversal message requires field 90",
),
)
}
}
Err(error) => issues.push(domain_issue(0, error.code(), error.message()))
}
issues
}