use crate::types::TransactionOutcome;
fn at(p: &str, pos1: usize, len: usize) -> &str {
if pos1 == 0 || pos1 > p.len() {
return "";
}
let i = pos1 - 1;
let end = (i + len).min(p.len());
p.get(i..end).unwrap_or("")
}
fn trim_right(s: &str) -> String {
s.trim_end_matches(' ').to_string()
}
fn xml_value(xml: &str, key: &str) -> String {
let needle = format!("\"{key}\">");
let Some(start) = xml.find(&needle) else {
return String::new();
};
let from = start + needle.len();
let rest = &xml[from..];
let value = match rest.find('<') {
Some(end) => &rest[..end],
None => rest,
};
value.trim().to_string()
}
#[must_use]
pub fn outcome_from_code(code: &str) -> TransactionOutcome {
match code {
"00" => TransactionOutcome::Ok,
"01" => TransactionOutcome::Ko,
"05" => TransactionOutcome::CardNotPresent,
"09" => TransactionOutcome::UnknownTag,
_ => TransactionOutcome::Unknown,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DccInfo {
pub applied: bool,
pub rate: String,
pub currency_code: String,
pub amount: String,
pub precision: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PaymentResponse {
pub outcome: TransactionOutcome,
pub result_code: String,
pub pan: String,
pub transaction_type: String,
pub auth_code: String,
pub host_date_time: String,
pub error_description: String,
pub card_type: String,
pub acquirer_id: String,
pub stan: String,
pub online_id: String,
pub currency: DccInfo,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StatusResponse {
pub terminal_id: String,
pub date_time_raw: String,
pub status: i32,
pub software_release: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TotalsResponse {
pub outcome: TransactionOutcome,
pub result_code: String,
pub pos_total: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CloseResponse {
pub outcome: TransactionOutcome,
pub result_code: String,
pub pos_total: String,
pub host_total: String,
pub error_description: String,
pub action_code: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PreAuthResponse {
pub outcome: TransactionOutcome,
pub result_code: String,
pub pan: String,
pub transaction_type: String,
pub auth_code: String,
pub pre_authorized_amount: String,
pub pre_auth_code: String,
pub action_code: String,
pub host_date_time: String,
pub error_description: String,
pub card_type: String,
pub acquirer_id: String,
pub stan: String,
pub online_id: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VasResponse {
pub response_id: String,
pub response_message: String,
pub order_id: String,
pub more_messages: bool,
pub id_message: String,
pub raw_xml: String,
}
#[must_use]
pub fn parse_payment(p: &str) -> PaymentResponse {
let mut r = PaymentResponse::default();
let dcc = at(p, 10, 1) == "V";
r.result_code = at(p, 11, 2).to_string();
r.outcome = outcome_from_code(&r.result_code);
if r.outcome == TransactionOutcome::Ko {
r.error_description = trim_right(at(p, 13, 24));
} else {
r.pan = at(p, 13, 19).to_string();
r.transaction_type = trim_right(at(p, 32, 3));
r.auth_code = trim_right(at(p, 35, 6));
r.host_date_time = at(p, 41, 7).to_string();
}
r.card_type = at(p, 48, 1).to_string();
r.acquirer_id = trim_right(at(p, 49, 11));
r.stan = at(p, 60, 6).to_string();
r.online_id = at(p, 66, 6).to_string();
if dcc {
r.currency.applied = at(p, 83, 1) == "1";
r.currency.rate = at(p, 84, 8).to_string();
r.currency.currency_code = trim_right(at(p, 92, 3));
r.currency.amount = at(p, 95, 12).to_string();
r.currency.precision = at(p, 107, 1).to_string();
}
r
}
#[must_use]
pub fn parse_status(p: &str) -> StatusResponse {
let s = at(p, 31, 1);
let status = s
.bytes()
.next()
.filter(u8::is_ascii_digit)
.map_or(-1, |b| i32::from(b - b'0'));
StatusResponse {
terminal_id: at(p, 1, 8).to_string(),
date_time_raw: at(p, 21, 10).to_string(),
status,
software_release: trim_right(at(p, 32, p.len())),
}
}
#[must_use]
pub fn parse_totals(p: &str) -> TotalsResponse {
let result_code = at(p, 11, 2).to_string();
TotalsResponse {
outcome: outcome_from_code(&result_code),
result_code,
pos_total: at(p, 13, 16).to_string(),
}
}
#[must_use]
pub fn parse_close(p: &str) -> CloseResponse {
let mut r = CloseResponse {
result_code: at(p, 11, 2).to_string(),
..Default::default()
};
r.outcome = outcome_from_code(&r.result_code);
if r.outcome == TransactionOutcome::Ok {
r.pos_total = at(p, 13, 16).to_string();
r.host_total = at(p, 29, 16).to_string();
} else {
r.error_description = trim_right(at(p, 13, 19));
r.action_code = at(p, 32, 3).to_string();
}
r
}
#[must_use]
pub fn parse_pre_auth(p: &str) -> PreAuthResponse {
let mut r = PreAuthResponse {
result_code: at(p, 11, 2).to_string(),
..Default::default()
};
r.outcome = outcome_from_code(&r.result_code);
if r.outcome == TransactionOutcome::Ko {
r.error_description = trim_right(at(p, 13, 24));
r.action_code = at(p, 37, 3).to_string();
} else {
r.pan = at(p, 13, 19).to_string();
r.transaction_type = trim_right(at(p, 32, 3));
r.auth_code = trim_right(at(p, 35, 6));
r.pre_authorized_amount = at(p, 41, 8).to_string();
r.pre_auth_code = at(p, 49, 9).to_string();
r.action_code = at(p, 58, 3).to_string();
r.host_date_time = at(p, 61, 7).to_string();
}
if r.outcome == TransactionOutcome::Ko {
r.card_type = at(p, 48, 1).to_string();
}
r.acquirer_id = trim_right(at(p, 72, 11));
r.stan = at(p, 83, 6).to_string();
r.online_id = at(p, 89, 6).to_string();
r
}
#[must_use]
pub fn parse_vas(p: &str) -> VasResponse {
let raw_xml = at(p, 27, p.len()).to_string();
VasResponse {
response_id: xml_value(&raw_xml, "RESPID"),
response_message: xml_value(&raw_xml, "RESPMSG"),
order_id: xml_value(&raw_xml, "ORDER_ID"),
more_messages: at(p, 15, 1) == "1",
id_message: at(p, 16, 3).to_string(),
raw_xml,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn a(value: &str, width: usize) -> String {
let mut s = value.to_string();
s.push_str(&" ".repeat(width.saturating_sub(value.len())));
s
}
fn n(value: &str, width: usize) -> String {
format!("{}{}", "0".repeat(width.saturating_sub(value.len())), value)
}
#[test]
fn payment_positive() {
let p = format!(
"{}0E00{}{}{}2111520{}{}{}{}",
a("12345678", 8),
n("4111111111", 19),
a("ICC", 3),
a("ABC123", 6),
"2",
a("ACQ", 11),
n("42", 6),
n("99", 6)
);
let r = parse_payment(&p);
assert_eq!(r.outcome, TransactionOutcome::Ok);
assert_eq!(r.result_code, "00");
assert_eq!(r.pan, n("4111111111", 19));
assert_eq!(r.transaction_type, "ICC");
assert_eq!(r.auth_code, "ABC123");
assert_eq!(r.host_date_time, "2111520");
assert_eq!(r.card_type, "2");
assert_eq!(r.acquirer_id, "ACQ");
assert_eq!(r.stan, "000042");
assert_eq!(r.online_id, "000099");
assert!(!r.currency.applied);
}
#[test]
fn payment_negative() {
let p = format!(
"{}0E01{}{}3{}{}{}",
a("12345678", 8),
a("CARTA RIFIUTATA", 24),
n("", 11), a("AC2", 11),
n("7", 6),
n("3", 6)
);
let r = parse_payment(&p);
assert_eq!(r.outcome, TransactionOutcome::Ko);
assert_eq!(r.result_code, "01");
assert_eq!(r.error_description, "CARTA RIFIUTATA");
assert_eq!(r.card_type, "3");
assert_eq!(r.stan, "000007");
}
#[test]
fn payment_with_currency_exchange() {
let base = format!(
"{}0V00{}{}{}2111520{}{}{}{}",
a("12345678", 8),
n("4111111111", 19),
a("ICC", 3),
a("ABC123", 6),
"2",
a("ACQ", 11),
n("42", 6),
n("99", 6)
);
let p = format!(
"{base}000{}1{}USD{}2",
n("650", 8),
n("12345", 8),
n("650", 12)
);
let r = parse_payment(&p);
assert_eq!(r.outcome, TransactionOutcome::Ok);
assert!(r.currency.applied);
assert_eq!(r.currency.rate, "00012345");
assert_eq!(r.currency.currency_code, "USD");
assert_eq!(r.currency.amount, "000000000650");
assert_eq!(r.currency.precision, "2");
}
#[test]
fn status() {
let p = format!("{}0s{}0102251530{}V1.2.3", a("12345678", 8), n("", 10), "2");
let r = parse_status(&p);
assert_eq!(r.terminal_id, "12345678");
assert_eq!(r.date_time_raw, "0102251530");
assert_eq!(r.status, 2);
assert_eq!(r.software_release, "V1.2.3");
}
#[test]
fn totals() {
let p = format!("{}0T00{}{}", a("12345678", 8), n("123456", 16), n("", 6));
let r = parse_totals(&p);
assert_eq!(r.outcome, TransactionOutcome::Ok);
assert_eq!(r.pos_total, n("123456", 16));
}
#[test]
fn close_positive() {
let p = format!("{}0C00{}{}", a("12345678", 8), n("1000", 16), n("1000", 16));
let r = parse_close(&p);
assert_eq!(r.outcome, TransactionOutcome::Ok);
assert_eq!(r.pos_total, n("1000", 16));
assert_eq!(r.host_total, n("1000", 16));
}
#[test]
fn close_negative() {
let p = format!("{}0C01{}100", a("12345678", 8), a("SBILANCIO", 19));
let r = parse_close(&p);
assert_eq!(r.outcome, TransactionOutcome::Ko);
assert_eq!(r.error_description, "SBILANCIO");
assert_eq!(r.action_code, "100");
}
#[test]
fn pre_auth_positive() {
let p = format!(
"{}0e00{}{}{}{}{}000{}",
a("12345678", 8),
n("4111111111", 19),
a("CLI", 3),
a("AUTH01", 6),
n("50000", 8),
n("123", 9),
"2111520"
);
let r = parse_pre_auth(&p);
assert_eq!(r.outcome, TransactionOutcome::Ok);
assert_eq!(r.transaction_type, "CLI");
assert_eq!(r.auth_code, "AUTH01");
assert_eq!(r.pre_authorized_amount, "00050000");
assert_eq!(r.pre_auth_code, "000000123");
assert_eq!(r.host_date_time, "2111520");
}
#[test]
fn pre_auth_positive_does_not_leak_amount_digit_as_card_type() {
let p = format!(
"{}0e00{}{}{}{}{}000{}",
a("12345678", 8),
n("4111111111", 19),
a("CLI", 3),
a("AUTH01", 6),
n("50001", 8),
n("123", 9),
"2111520"
);
let r = parse_pre_auth(&p);
assert_eq!(r.outcome, TransactionOutcome::Ok);
assert_eq!(r.pre_authorized_amount, "00050001"); assert_eq!(r.card_type, ""); }
#[test]
fn pre_auth_negative() {
let p = format!(
"{}0e01{}100{}3{}{}{}{}",
a("12345678", 8),
a("NEGATO", 24),
n("", 8),
n("", 23),
a("ACQ", 11),
n("55", 6),
n("66", 6)
);
let r = parse_pre_auth(&p);
assert_eq!(r.outcome, TransactionOutcome::Ko);
assert_eq!(r.error_description, "NEGATO");
assert_eq!(r.action_code, "100");
assert_eq!(r.card_type, "3"); assert_eq!(r.acquirer_id, "ACQ");
assert_eq!(r.stan, "000055");
assert_eq!(r.online_id, "000066");
assert_eq!(r.pan, ""); }
#[test]
fn vas() {
let xml = "<ecrres><p k=\"RESPID\">0</p><p k=\"RESPMSG\">OK-APPROVED</p>\
<p k=\"ORDER_ID\">ABC123</p></ecrres>";
let p = format!("{}0K{}0001{}{}", a("12345678", 8), n("", 4), n("", 8), xml);
let r = parse_vas(&p);
assert!(!r.more_messages);
assert_eq!(r.id_message, "001");
assert_eq!(r.response_id, "0");
assert_eq!(r.response_message, "OK-APPROVED");
assert_eq!(r.order_id, "ABC123");
assert_eq!(r.raw_xml, xml);
}
#[test]
fn defensive_on_short_or_empty_payload() {
let r = parse_payment("");
assert_eq!(r.outcome, TransactionOutcome::Unknown);
assert_eq!(r.result_code, "");
assert_eq!(r.pan, "");
let s = parse_status("123"); assert_eq!(s.status, -1);
}
#[test]
fn outcome_mapping() {
assert_eq!(outcome_from_code("00"), TransactionOutcome::Ok);
assert_eq!(outcome_from_code("01"), TransactionOutcome::Ko);
assert_eq!(outcome_from_code("05"), TransactionOutcome::CardNotPresent);
assert_eq!(outcome_from_code("09"), TransactionOutcome::UnknownTag);
assert_eq!(outcome_from_code("zz"), TransactionOutcome::Unknown);
}
}