1use crate::types::TransactionOutcome;
25
26fn at(p: &str, pos1: usize, len: usize) -> &str {
30 if pos1 == 0 || pos1 > p.len() {
31 return "";
32 }
33 let i = pos1 - 1;
34 let end = (i + len).min(p.len());
35 p.get(i..end).unwrap_or("")
36}
37
38fn trim_right(s: &str) -> String {
39 s.trim_end_matches(' ').to_string()
40}
41
42fn xml_value(xml: &str, key: &str) -> String {
44 let needle = format!("\"{key}\">");
45 let Some(start) = xml.find(&needle) else {
46 return String::new();
47 };
48 let from = start + needle.len();
49 let rest = &xml[from..];
50 let value = match rest.find('<') {
51 Some(end) => &rest[..end],
52 None => rest,
53 };
54 value.trim().to_string()
55}
56
57#[must_use]
59pub fn outcome_from_code(code: &str) -> TransactionOutcome {
60 match code {
61 "00" => TransactionOutcome::Ok,
62 "01" => TransactionOutcome::Ko,
63 "05" => TransactionOutcome::CardNotPresent,
64 "09" => TransactionOutcome::UnknownTag,
65 _ => TransactionOutcome::Unknown,
66 }
67}
68
69#[derive(Debug, Clone, Default, PartialEq, Eq)]
72pub struct DccInfo {
73 pub applied: bool,
75 pub rate: String,
77 pub currency_code: String,
79 pub amount: String,
81 pub precision: String,
83}
84
85#[derive(Debug, Clone, Default, PartialEq, Eq)]
88pub struct PaymentResponse {
89 pub outcome: TransactionOutcome,
91 pub result_code: String,
93 pub pan: String,
95 pub transaction_type: String,
97 pub auth_code: String,
99 pub host_date_time: String,
101 pub error_description: String,
103 pub card_type: String,
105 pub acquirer_id: String,
107 pub stan: String,
109 pub online_id: String,
111 pub currency: DccInfo,
113}
114
115#[derive(Debug, Clone, Default, PartialEq, Eq)]
117pub struct StatusResponse {
118 pub terminal_id: String,
120 pub date_time_raw: String,
122 pub status: i32,
124 pub software_release: String,
126}
127
128#[derive(Debug, Clone, Default, PartialEq, Eq)]
130pub struct TotalsResponse {
131 pub outcome: TransactionOutcome,
133 pub result_code: String,
135 pub pos_total: String,
137}
138
139#[derive(Debug, Clone, Default, PartialEq, Eq)]
141pub struct CloseResponse {
142 pub outcome: TransactionOutcome,
144 pub result_code: String,
146 pub pos_total: String,
148 pub host_total: String,
150 pub error_description: String,
152 pub action_code: String,
154}
155
156#[derive(Debug, Clone, Default, PartialEq, Eq)]
158pub struct PreAuthResponse {
159 pub outcome: TransactionOutcome,
161 pub result_code: String,
163 pub pan: String,
165 pub transaction_type: String,
167 pub auth_code: String,
169 pub pre_authorized_amount: String,
171 pub pre_auth_code: String,
173 pub action_code: String,
175 pub host_date_time: String,
177 pub error_description: String,
179 pub card_type: String,
181 pub acquirer_id: String,
183 pub stan: String,
185 pub online_id: String,
187}
188
189#[derive(Debug, Clone, Default, PartialEq, Eq)]
191pub struct VasResponse {
192 pub response_id: String,
194 pub response_message: String,
196 pub order_id: String,
198 pub more_messages: bool,
200 pub id_message: String,
202 pub raw_xml: String,
204}
205
206#[must_use]
208pub fn parse_payment(p: &str) -> PaymentResponse {
209 let mut r = PaymentResponse::default();
210 let dcc = at(p, 10, 1) == "V"; r.result_code = at(p, 11, 2).to_string();
213 r.outcome = outcome_from_code(&r.result_code);
214
215 if r.outcome == TransactionOutcome::Ko {
216 r.error_description = trim_right(at(p, 13, 24));
217 } else {
218 r.pan = at(p, 13, 19).to_string();
219 r.transaction_type = trim_right(at(p, 32, 3));
220 r.auth_code = trim_right(at(p, 35, 6));
221 r.host_date_time = at(p, 41, 7).to_string();
222 }
223
224 r.card_type = at(p, 48, 1).to_string();
226 r.acquirer_id = trim_right(at(p, 49, 11));
227 r.stan = at(p, 60, 6).to_string();
228 r.online_id = at(p, 66, 6).to_string();
229
230 if dcc {
231 r.currency.applied = at(p, 83, 1) == "1";
232 r.currency.rate = at(p, 84, 8).to_string();
233 r.currency.currency_code = trim_right(at(p, 92, 3));
234 r.currency.amount = at(p, 95, 12).to_string();
235 r.currency.precision = at(p, 107, 1).to_string();
236 }
237 r
238}
239
240#[must_use]
242pub fn parse_status(p: &str) -> StatusResponse {
243 let s = at(p, 31, 1);
244 let status = s
245 .bytes()
246 .next()
247 .filter(u8::is_ascii_digit)
248 .map_or(-1, |b| i32::from(b - b'0'));
249 StatusResponse {
250 terminal_id: at(p, 1, 8).to_string(),
251 date_time_raw: at(p, 21, 10).to_string(),
252 status,
253 software_release: trim_right(at(p, 32, p.len())),
254 }
255}
256
257#[must_use]
259pub fn parse_totals(p: &str) -> TotalsResponse {
260 let result_code = at(p, 11, 2).to_string();
261 TotalsResponse {
262 outcome: outcome_from_code(&result_code),
263 result_code,
264 pos_total: at(p, 13, 16).to_string(),
265 }
266}
267
268#[must_use]
270pub fn parse_close(p: &str) -> CloseResponse {
271 let mut r = CloseResponse {
272 result_code: at(p, 11, 2).to_string(),
273 ..Default::default()
274 };
275 r.outcome = outcome_from_code(&r.result_code);
276 if r.outcome == TransactionOutcome::Ok {
277 r.pos_total = at(p, 13, 16).to_string();
278 r.host_total = at(p, 29, 16).to_string();
279 } else {
280 r.error_description = trim_right(at(p, 13, 19));
281 r.action_code = at(p, 32, 3).to_string();
282 }
283 r
284}
285
286#[must_use]
288pub fn parse_pre_auth(p: &str) -> PreAuthResponse {
289 let mut r = PreAuthResponse {
290 result_code: at(p, 11, 2).to_string(),
291 ..Default::default()
292 };
293 r.outcome = outcome_from_code(&r.result_code);
294 if r.outcome == TransactionOutcome::Ko {
295 r.error_description = trim_right(at(p, 13, 24));
296 r.action_code = at(p, 37, 3).to_string();
297 } else {
298 r.pan = at(p, 13, 19).to_string();
299 r.transaction_type = trim_right(at(p, 32, 3));
300 r.auth_code = trim_right(at(p, 35, 6));
301 r.pre_authorized_amount = at(p, 41, 8).to_string();
302 r.pre_auth_code = at(p, 49, 9).to_string();
303 r.action_code = at(p, 58, 3).to_string();
304 r.host_date_time = at(p, 61, 7).to_string();
305 }
306 if r.outcome == TransactionOutcome::Ko {
309 r.card_type = at(p, 48, 1).to_string();
310 }
311 r.acquirer_id = trim_right(at(p, 72, 11));
312 r.stan = at(p, 83, 6).to_string();
313 r.online_id = at(p, 89, 6).to_string();
314 r
315}
316
317#[must_use]
319pub fn parse_vas(p: &str) -> VasResponse {
320 let raw_xml = at(p, 27, p.len()).to_string();
321 VasResponse {
322 response_id: xml_value(&raw_xml, "RESPID"),
323 response_message: xml_value(&raw_xml, "RESPMSG"),
324 order_id: xml_value(&raw_xml, "ORDER_ID"),
325 more_messages: at(p, 15, 1) == "1",
326 id_message: at(p, 16, 3).to_string(),
327 raw_xml,
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 fn a(value: &str, width: usize) -> String {
337 let mut s = value.to_string();
338 s.push_str(&" ".repeat(width.saturating_sub(value.len())));
339 s
340 }
341
342 fn n(value: &str, width: usize) -> String {
344 format!("{}{}", "0".repeat(width.saturating_sub(value.len())), value)
345 }
346
347 #[test]
348 fn payment_positive() {
349 let p = format!(
350 "{}0E00{}{}{}2111520{}{}{}{}",
351 a("12345678", 8),
352 n("4111111111", 19),
353 a("ICC", 3),
354 a("ABC123", 6),
355 "2",
356 a("ACQ", 11),
357 n("42", 6),
358 n("99", 6)
359 );
360 let r = parse_payment(&p);
361 assert_eq!(r.outcome, TransactionOutcome::Ok);
362 assert_eq!(r.result_code, "00");
363 assert_eq!(r.pan, n("4111111111", 19));
364 assert_eq!(r.transaction_type, "ICC");
365 assert_eq!(r.auth_code, "ABC123");
366 assert_eq!(r.host_date_time, "2111520");
367 assert_eq!(r.card_type, "2");
368 assert_eq!(r.acquirer_id, "ACQ");
369 assert_eq!(r.stan, "000042");
370 assert_eq!(r.online_id, "000099");
371 assert!(!r.currency.applied);
372 }
373
374 #[test]
375 fn payment_negative() {
376 let p = format!(
377 "{}0E01{}{}3{}{}{}",
378 a("12345678", 8),
379 a("CARTA RIFIUTATA", 24),
380 n("", 11), a("AC2", 11),
382 n("7", 6),
383 n("3", 6)
384 );
385 let r = parse_payment(&p);
386 assert_eq!(r.outcome, TransactionOutcome::Ko);
387 assert_eq!(r.result_code, "01");
388 assert_eq!(r.error_description, "CARTA RIFIUTATA");
389 assert_eq!(r.card_type, "3");
390 assert_eq!(r.stan, "000007");
391 }
392
393 #[test]
394 fn payment_with_currency_exchange() {
395 let base = format!(
396 "{}0V00{}{}{}2111520{}{}{}{}",
397 a("12345678", 8),
398 n("4111111111", 19),
399 a("ICC", 3),
400 a("ABC123", 6),
401 "2",
402 a("ACQ", 11),
403 n("42", 6),
404 n("99", 6)
405 );
406 let p = format!(
408 "{base}000{}1{}USD{}2",
409 n("650", 8),
410 n("12345", 8),
411 n("650", 12)
412 );
413 let r = parse_payment(&p);
414 assert_eq!(r.outcome, TransactionOutcome::Ok);
415 assert!(r.currency.applied);
416 assert_eq!(r.currency.rate, "00012345");
417 assert_eq!(r.currency.currency_code, "USD");
418 assert_eq!(r.currency.amount, "000000000650");
419 assert_eq!(r.currency.precision, "2");
420 }
421
422 #[test]
423 fn status() {
424 let p = format!("{}0s{}0102251530{}V1.2.3", a("12345678", 8), n("", 10), "2");
425 let r = parse_status(&p);
426 assert_eq!(r.terminal_id, "12345678");
427 assert_eq!(r.date_time_raw, "0102251530");
428 assert_eq!(r.status, 2);
429 assert_eq!(r.software_release, "V1.2.3");
430 }
431
432 #[test]
433 fn totals() {
434 let p = format!("{}0T00{}{}", a("12345678", 8), n("123456", 16), n("", 6));
435 let r = parse_totals(&p);
436 assert_eq!(r.outcome, TransactionOutcome::Ok);
437 assert_eq!(r.pos_total, n("123456", 16));
438 }
439
440 #[test]
441 fn close_positive() {
442 let p = format!("{}0C00{}{}", a("12345678", 8), n("1000", 16), n("1000", 16));
443 let r = parse_close(&p);
444 assert_eq!(r.outcome, TransactionOutcome::Ok);
445 assert_eq!(r.pos_total, n("1000", 16));
446 assert_eq!(r.host_total, n("1000", 16));
447 }
448
449 #[test]
450 fn close_negative() {
451 let p = format!("{}0C01{}100", a("12345678", 8), a("SBILANCIO", 19));
452 let r = parse_close(&p);
453 assert_eq!(r.outcome, TransactionOutcome::Ko);
454 assert_eq!(r.error_description, "SBILANCIO");
455 assert_eq!(r.action_code, "100");
456 }
457
458 #[test]
459 fn pre_auth_positive() {
460 let p = format!(
461 "{}0e00{}{}{}{}{}000{}",
462 a("12345678", 8),
463 n("4111111111", 19),
464 a("CLI", 3),
465 a("AUTH01", 6),
466 n("50000", 8),
467 n("123", 9),
468 "2111520"
469 );
470 let r = parse_pre_auth(&p);
471 assert_eq!(r.outcome, TransactionOutcome::Ok);
472 assert_eq!(r.transaction_type, "CLI");
473 assert_eq!(r.auth_code, "AUTH01");
474 assert_eq!(r.pre_authorized_amount, "00050000");
475 assert_eq!(r.pre_auth_code, "000000123");
476 assert_eq!(r.host_date_time, "2111520");
477 }
478
479 #[test]
483 fn pre_auth_positive_does_not_leak_amount_digit_as_card_type() {
484 let p = format!(
485 "{}0e00{}{}{}{}{}000{}",
486 a("12345678", 8),
487 n("4111111111", 19),
488 a("CLI", 3),
489 a("AUTH01", 6),
490 n("50001", 8),
491 n("123", 9),
492 "2111520"
493 );
494 let r = parse_pre_auth(&p);
495 assert_eq!(r.outcome, TransactionOutcome::Ok);
496 assert_eq!(r.pre_authorized_amount, "00050001"); assert_eq!(r.card_type, ""); }
499
500 #[test]
501 fn pre_auth_negative() {
502 let p = format!(
505 "{}0e01{}100{}3{}{}{}{}",
506 a("12345678", 8),
507 a("NEGATO", 24),
508 n("", 8),
509 n("", 23),
510 a("ACQ", 11),
511 n("55", 6),
512 n("66", 6)
513 );
514 let r = parse_pre_auth(&p);
515 assert_eq!(r.outcome, TransactionOutcome::Ko);
516 assert_eq!(r.error_description, "NEGATO");
517 assert_eq!(r.action_code, "100");
518 assert_eq!(r.card_type, "3"); assert_eq!(r.acquirer_id, "ACQ");
520 assert_eq!(r.stan, "000055");
521 assert_eq!(r.online_id, "000066");
522 assert_eq!(r.pan, ""); }
524
525 #[test]
526 fn vas() {
527 let xml = "<ecrres><p k=\"RESPID\">0</p><p k=\"RESPMSG\">OK-APPROVED</p>\
528 <p k=\"ORDER_ID\">ABC123</p></ecrres>";
529 let p = format!("{}0K{}0001{}{}", a("12345678", 8), n("", 4), n("", 8), xml);
531 let r = parse_vas(&p);
532 assert!(!r.more_messages);
533 assert_eq!(r.id_message, "001");
534 assert_eq!(r.response_id, "0");
535 assert_eq!(r.response_message, "OK-APPROVED");
536 assert_eq!(r.order_id, "ABC123");
537 assert_eq!(r.raw_xml, xml);
538 }
539
540 #[test]
541 fn defensive_on_short_or_empty_payload() {
542 let r = parse_payment("");
543 assert_eq!(r.outcome, TransactionOutcome::Unknown);
544 assert_eq!(r.result_code, "");
545 assert_eq!(r.pan, "");
546
547 let s = parse_status("123"); assert_eq!(s.status, -1);
549 }
550
551 #[test]
552 fn outcome_mapping() {
553 assert_eq!(outcome_from_code("00"), TransactionOutcome::Ok);
554 assert_eq!(outcome_from_code("01"), TransactionOutcome::Ko);
555 assert_eq!(outcome_from_code("05"), TransactionOutcome::CardNotPresent);
556 assert_eq!(outcome_from_code("09"), TransactionOutcome::UnknownTag);
557 assert_eq!(outcome_from_code("zz"), TransactionOutcome::Unknown);
558 }
559}