use crate::error::{Ecr17Error, Result};
const RESERVED: char = '0';
const FIELD_SEP: char = '\u{1B}';
fn left_pad(value: &str, size: usize, ch: char) -> Result<String> {
if value.len() > size {
return Err(Ecr17Error::FieldOverflow {
value: value.to_string(),
width: size,
});
}
let mut s = String::with_capacity(size);
for _ in 0..size - value.len() {
s.push(ch);
}
s.push_str(value);
Ok(s)
}
fn right_pad(value: &str, size: usize, ch: char) -> Result<String> {
if value.len() > size {
return Err(Ecr17Error::FieldOverflow {
value: value.to_string(),
width: size,
});
}
let mut s = String::with_capacity(size);
s.push_str(value);
for _ in 0..size - value.len() {
s.push(ch);
}
Ok(s)
}
fn amount_field(amount_cents: i64) -> Result<String> {
if amount_cents < 0 {
return Err(Ecr17Error::NegativeAmount);
}
left_pad(&amount_cents.to_string(), 8, RESERVED)
}
fn flag(on: bool) -> char {
if on {
'1'
} else {
'0'
}
}
fn validate_payment_type(payment_type: char) -> Result<()> {
if matches!(payment_type, '0'..='3') {
Ok(())
} else {
Err(Ecr17Error::InvalidPaymentType {
value: payment_type,
})
}
}
#[allow(clippy::too_many_arguments)]
fn build_payment_like(
code: char,
terminal_id: &str,
cash_register_id: &str,
amount_cents: i64,
payment_type: char,
card_already_present: bool,
with_additional_data: bool,
receipt_text: &str,
) -> Result<String> {
validate_payment_type(payment_type)?;
let mut m = String::with_capacity(167);
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push(code); m.push_str(&left_pad(cash_register_id, 8, RESERVED)?); m.push(flag(with_additional_data)); m.push_str("00"); m.push(flag(card_already_present)); m.push(payment_type); m.push_str(&amount_field(amount_cents)?); m.push_str(&left_pad(receipt_text, 128, ' ')?);
m.push_str("00000000"); Ok(m) }
fn build_pre_auth_follow_up(
code: char,
terminal_id: &str,
cash_register_id: &str,
amount_cents: i64,
original_pre_auth_code: &str,
with_additional_data: bool,
receipt_text: &str,
) -> Result<String> {
let mut m = String::with_capacity(176);
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push(code); m.push_str(&left_pad(cash_register_id, 8, RESERVED)?); m.push(flag(with_additional_data)); m.push_str("0000"); m.push_str(&amount_field(amount_cents)?); m.push_str(&left_pad(receipt_text, 128, ' ')?); m.push_str(&left_pad(original_pre_auth_code, 9, RESERVED)?); m.push_str("00000000"); Ok(m) }
fn build_session_command(
code: char,
terminal_id: &str,
cash_register_id: &str,
with_additional_data: bool,
) -> Result<String> {
let mut m = String::with_capacity(26);
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push(code); m.push_str(&left_pad(cash_register_id, 8, RESERVED)?); m.push(flag(with_additional_data)); m.push_str("0000000"); Ok(m) }
#[allow(clippy::too_many_arguments)]
pub fn build_payment(
terminal_id: &str,
cash_register_id: &str,
amount_cents: i64,
payment_type: char,
card_already_present: bool,
with_additional_data: bool,
receipt_text: &str,
) -> Result<String> {
build_payment_like(
'P',
terminal_id,
cash_register_id,
amount_cents,
payment_type,
card_already_present,
with_additional_data,
receipt_text,
)
}
#[allow(clippy::too_many_arguments)]
pub fn build_extended_payment(
terminal_id: &str,
cash_register_id: &str,
amount_cents: i64,
payment_type: char,
card_already_present: bool,
with_additional_data: bool,
receipt_text: &str,
) -> Result<String> {
build_payment_like(
'X',
terminal_id,
cash_register_id,
amount_cents,
payment_type,
card_already_present,
with_additional_data,
receipt_text,
)
}
#[allow(clippy::too_many_arguments)]
pub fn build_pre_auth(
terminal_id: &str,
cash_register_id: &str,
amount_cents: i64,
payment_type: char,
card_already_present: bool,
with_additional_data: bool,
receipt_text: &str,
) -> Result<String> {
build_payment_like(
'p',
terminal_id,
cash_register_id,
amount_cents,
payment_type,
card_already_present,
with_additional_data,
receipt_text,
)
}
pub fn build_incremental(
terminal_id: &str,
cash_register_id: &str,
amount_cents: i64,
original_pre_auth_code: &str,
with_additional_data: bool,
receipt_text: &str,
) -> Result<String> {
build_pre_auth_follow_up(
'i',
terminal_id,
cash_register_id,
amount_cents,
original_pre_auth_code,
with_additional_data,
receipt_text,
)
}
pub fn build_pre_auth_closure(
terminal_id: &str,
cash_register_id: &str,
amount_cents: i64,
original_pre_auth_code: &str,
with_additional_data: bool,
receipt_text: &str,
) -> Result<String> {
build_pre_auth_follow_up(
'c',
terminal_id,
cash_register_id,
amount_cents,
original_pre_auth_code,
with_additional_data,
receipt_text,
)
}
pub fn build_card_verification(
terminal_id: &str,
cash_register_id: &str,
payment_type: char,
with_additional_data: bool,
) -> Result<String> {
validate_payment_type(payment_type)?;
let mut m = String::with_capacity(39);
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push('H'); m.push_str(&left_pad(cash_register_id, 8, RESERVED)?); m.push(flag(with_additional_data)); m.push_str("00"); m.push('0'); m.push(payment_type); m.push_str("0000000000000000"); Ok(m) }
pub fn build_close_session(
terminal_id: &str,
cash_register_id: &str,
with_additional_data: bool,
) -> Result<String> {
build_session_command('C', terminal_id, cash_register_id, with_additional_data)
}
pub fn build_totals(
terminal_id: &str,
cash_register_id: &str,
with_additional_data: bool,
) -> Result<String> {
build_session_command('T', terminal_id, cash_register_id, with_additional_data)
}
pub fn build_send_last_result(
terminal_id: &str,
cash_register_id: &str,
with_additional_data: bool,
) -> Result<String> {
let mut m = String::with_capacity(22);
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push('G'); m.push_str(&left_pad(cash_register_id, 8, RESERVED)?); m.push(flag(with_additional_data)); m.push_str("000"); Ok(m) }
pub fn build_enable_ecr_print(terminal_id: &str, enabled: bool) -> Result<String> {
let mut m = String::with_capacity(11);
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push('E'); m.push(flag(enabled)); Ok(m) }
pub fn build_reprint(terminal_id: &str, to_ecr: bool, ticket_type: char) -> Result<String> {
let mut m = String::with_capacity(22);
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push('R'); m.push(flag(to_ecr)); m.push(ticket_type); m.push_str("0000000000"); Ok(m) }
pub fn build_status(terminal_id: &str) -> Result<String> {
let mut m = String::with_capacity(10);
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push('s'); Ok(m) }
pub fn build_reversal(terminal_id: &str, cash_register_id: &str, stan: &str) -> Result<String> {
let mut m = String::with_capacity(26);
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push('S'); m.push_str(&left_pad(cash_register_id, 8, RESERVED)?); m.push_str(&left_pad(stan, 6, RESERVED)?); m.push(RESERVED); m.push(RESERVED); Ok(m) }
pub fn build_vas(terminal_id: &str, ecr_id: &str, xml_request: &str) -> Result<String> {
if xml_request.len() > 1024 {
return Err(Ecr17Error::VasTooLong);
}
let mut m = String::new();
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push('K'); m.push_str(&left_pad(ecr_id, 8, RESERVED)?); m.push_str("000"); m.push(RESERVED); m.push_str(&left_pad(&xml_request.len().to_string(), 4, RESERVED)?); m.push_str(xml_request); Ok(m)
}
pub fn build_additional_tags(
terminal_id: &str,
tag_content: &str,
iso_field: &str,
tag_number: &str,
) -> Result<String> {
if tag_content.is_empty() || tag_content.len() > 100 || tag_content.contains(FIELD_SEP) {
return Err(Ecr17Error::TagContentInvalid);
}
let mut m = String::new();
m.push_str(&left_pad(terminal_id, 8, RESERVED)?); m.push(RESERVED); m.push('U'); m.push_str("000000"); m.push_str(&left_pad(iso_field, 2, RESERVED)?); m.push_str(&right_pad(tag_number, 8, ' ')?); m.push(RESERVED); m.push_str("0000"); m.push_str("00000"); m.push_str(tag_content); m.push(FIELD_SEP); Ok(m)
}
pub fn format_tokenization_tag(recurring: bool, contract_code: &str) -> Result<String> {
if contract_code.is_empty()
|| contract_code.len() > 18
|| !contract_code.bytes().all(|b| b.is_ascii_alphanumeric())
{
return Err(Ecr17Error::ContractCodeInvalid);
}
let service = if recurring { "0REC" } else { "0COF" };
Ok(format!("{service}0TRK{contract_code}|0FNZ03"))
}
#[cfg(test)]
mod tests {
use super::*;
const T: &str = "12345678"; const C: &str = "87654321";
#[test]
fn status_message_layout() {
let m = build_status("42").unwrap();
assert_eq!(m.len(), 10);
assert_eq!(&m[0..8], "00000042"); assert_eq!(m.as_bytes()[8], b'0'); assert_eq!(m.as_bytes()[9], b's'); }
#[test]
fn status_message_keeps_full_width_id() {
assert_eq!(build_status("12345678").unwrap(), "123456780s");
}
#[test]
fn payment_message_is_167_bytes() {
assert_eq!(
build_payment("1", "2", 650, '0', false, false, "")
.unwrap()
.len(),
167
);
}
#[test]
fn payment_message_field_layout() {
let m = build_payment("12345678", "87654321", 650, '0', false, false, "").unwrap();
assert_eq!(m.len(), 167);
assert_eq!(&m[0..8], "12345678"); assert_eq!(m.as_bytes()[8], b'0'); assert_eq!(m.as_bytes()[9], b'P'); assert_eq!(&m[10..18], "87654321"); assert_eq!(m.as_bytes()[18], b'0'); assert_eq!(&m[19..21], "00"); assert_eq!(m.as_bytes()[21], b'0'); assert_eq!(m.as_bytes()[22], b'0'); assert_eq!(&m[23..31], "00000650"); assert_eq!(&m[31..159], &" ".repeat(128)); assert_eq!(&m[159..167], "00000000"); }
#[test]
fn payment_message_amount_max_fits() {
let m = build_payment("1", "2", 99_999_999, '0', false, false, "").unwrap();
assert_eq!(&m[23..31], "99999999");
}
#[test]
fn payment_rejects_negative_amount() {
assert_eq!(
build_payment("1", "2", -1, '0', false, false, ""),
Err(Ecr17Error::NegativeAmount)
);
}
#[test]
fn payment_rejects_amount_overflowing_field() {
assert!(matches!(
build_payment("1", "2", 100_000_000, '0', false, false, ""),
Err(Ecr17Error::FieldOverflow { .. })
));
}
#[test]
fn payment_rejects_oversized_terminal_id() {
assert!(matches!(
build_payment("123456789", "2", 650, '0', false, false, ""),
Err(Ecr17Error::FieldOverflow { .. })
));
}
#[test]
fn status_rejects_oversized_terminal_id() {
assert!(matches!(
build_status("123456789"),
Err(Ecr17Error::FieldOverflow { .. })
));
}
#[test]
fn reversal_message_layout() {
let m = build_reversal("12345678", "87654321", "000123").unwrap();
assert_eq!(m.len(), 26);
assert_eq!(&m[0..8], "12345678");
assert_eq!(m.as_bytes()[8], b'0');
assert_eq!(m.as_bytes()[9], b'S');
assert_eq!(&m[10..18], "87654321");
assert_eq!(&m[18..24], "000123");
assert_eq!(m.as_bytes()[24], b'0');
assert_eq!(m.as_bytes()[25], b'0');
}
#[test]
fn reversal_default_stan_is_no_check() {
let m = build_reversal("12345678", "87654321", "000000").unwrap();
assert_eq!(&m[18..24], "000000");
}
#[test]
fn reversal_rejects_oversized_stan() {
assert!(matches!(
build_reversal("1", "2", "1234567"),
Err(Ecr17Error::FieldOverflow { .. })
));
}
#[test]
fn extended_payment_layout_and_flags() {
let m = build_extended_payment(T, C, 650, '2', true, true, "ABC").unwrap();
assert_eq!(m.len(), 167);
assert_eq!(&m[0..8], T);
assert_eq!(m.as_bytes()[8], b'0');
assert_eq!(m.as_bytes()[9], b'X');
assert_eq!(&m[10..18], C);
assert_eq!(m.as_bytes()[18], b'1'); assert_eq!(&m[19..21], "00");
assert_eq!(m.as_bytes()[21], b'1'); assert_eq!(m.as_bytes()[22], b'2'); assert_eq!(&m[23..31], "00000650"); assert_eq!(&m[31..156], &" ".repeat(125)); assert_eq!(&m[156..159], "ABC"); assert_eq!(&m[159..167], "00000000");
}
#[test]
fn payment_receipt_text_is_right_aligned() {
let m = build_payment(T, C, 650, '0', false, false, "ABC").unwrap();
assert_eq!(&m[31..156], &" ".repeat(125)); assert_eq!(&m[156..159], "ABC"); }
#[test]
fn pre_auth_uses_code_lower_p() {
let m = build_pre_auth(T, C, 1000, '0', false, false, "").unwrap();
assert_eq!(m.len(), 167);
assert_eq!(m.as_bytes()[9], b'p');
assert_eq!(&m[23..31], "00001000");
}
#[test]
fn payment_defaults_match_basic_layout() {
let m = build_payment(T, C, 650, '0', false, false, "").unwrap();
assert_eq!(m.len(), 167);
assert_eq!(m.as_bytes()[9], b'P');
assert_eq!(m.as_bytes()[18], b'0'); assert_eq!(m.as_bytes()[21], b'0'); assert_eq!(m.as_bytes()[22], b'0'); assert_eq!(&m[31..159], &" ".repeat(128));
}
#[test]
fn incremental_layout() {
let m = build_incremental(T, C, 1000, "123456789", false, "").unwrap();
assert_eq!(m.len(), 176);
assert_eq!(m.as_bytes()[9], b'i');
assert_eq!(&m[19..23], "0000");
assert_eq!(&m[23..31], "00001000");
assert_eq!(&m[159..168], "123456789"); assert_eq!(&m[168..176], "00000000");
}
#[test]
fn pre_auth_closure_layout() {
let m = build_pre_auth_closure(T, C, 500, "000000042", false, "").unwrap();
assert_eq!(m.len(), 176);
assert_eq!(m.as_bytes()[9], b'c');
assert_eq!(&m[159..168], "000000042");
}
#[test]
fn card_verification_layout() {
let m = build_card_verification(T, C, '1', false).unwrap();
assert_eq!(m.len(), 39);
assert_eq!(m.as_bytes()[9], b'H');
assert_eq!(&m[10..18], C);
assert_eq!(m.as_bytes()[18], b'0'); assert_eq!(&m[19..21], "00");
assert_eq!(m.as_bytes()[21], b'0'); assert_eq!(m.as_bytes()[22], b'1'); assert_eq!(&m[23..39], &"0".repeat(16));
}
#[test]
fn close_session_layout() {
let m = build_close_session(T, C, false).unwrap();
assert_eq!(m.len(), 26);
assert_eq!(m.as_bytes()[9], b'C');
assert_eq!(&m[10..18], C);
assert_eq!(m.as_bytes()[18], b'0');
assert_eq!(&m[19..26], &"0".repeat(7));
}
#[test]
fn totals_layout() {
let m = build_totals(T, C, false).unwrap();
assert_eq!(m.len(), 26);
assert_eq!(m.as_bytes()[9], b'T');
}
#[test]
fn send_last_result_layout() {
let m = build_send_last_result(T, C, false).unwrap();
assert_eq!(m.len(), 22);
assert_eq!(m.as_bytes()[9], b'G');
assert_eq!(&m[19..22], "000");
}
#[test]
fn enable_ecr_print_layout() {
assert_eq!(build_enable_ecr_print(T, true).unwrap(), "123456780E1");
assert_eq!(build_enable_ecr_print(T, false).unwrap(), "123456780E0");
}
#[test]
fn reprint_layout() {
let m = build_reprint(T, true, '0').unwrap();
assert_eq!(m.len(), 22);
assert_eq!(m.as_bytes()[9], b'R');
assert_eq!(m.as_bytes()[10], b'1'); assert_eq!(m.as_bytes()[11], b'0'); assert_eq!(&m[12..22], &"0".repeat(10));
}
#[test]
fn vas_layout_and_length_prefix() {
let m = build_vas(T, C, "<x/>").unwrap();
assert_eq!(m.len(), 30);
assert_eq!(m.as_bytes()[9], b'K');
assert_eq!(&m[10..18], C);
assert_eq!(&m[18..21], "000");
assert_eq!(m.as_bytes()[21], b'0');
assert_eq!(&m[22..26], "0004"); assert_eq!(&m[26..], "<x/>");
}
#[test]
fn vas_rejects_oversized_request() {
assert_eq!(
build_vas(T, C, &"x".repeat(1025)),
Err(Ecr17Error::VasTooLong)
);
}
#[test]
fn additional_tags_layout() {
let content = "0COF0TRK123|0FNZ03"; let m = build_additional_tags(T, content, "62", "DF8D01").unwrap();
assert_eq!(m.len(), 36 + content.len() + 1);
assert_eq!(m.as_bytes()[9], b'U');
assert_eq!(&m[10..16], "000000");
assert_eq!(&m[16..18], "62");
assert_eq!(&m[18..26], "DF8D01 "); assert_eq!(m.as_bytes()[26], b'0');
assert_eq!(&m[27..31], "0000");
assert_eq!(&m[31..36], "00000");
assert_eq!(&m[36..36 + content.len()], content);
assert_eq!(m.as_bytes()[m.len() - 1], 0x1B);
}
#[test]
fn additional_tags_rejects_bad_content() {
assert_eq!(
build_additional_tags(T, "", "62", "DF8D01"),
Err(Ecr17Error::TagContentInvalid)
);
assert_eq!(
build_additional_tags(T, &"x".repeat(101), "62", "DF8D01"),
Err(Ecr17Error::TagContentInvalid)
);
assert_eq!(
build_additional_tags(T, "abc\u{1B}def", "62", "DF8D01"),
Err(Ecr17Error::TagContentInvalid)
);
}
#[test]
fn tokenization_tag_format() {
assert_eq!(
format_tokenization_tag(false, "1666354841608").unwrap(),
"0COF0TRK1666354841608|0FNZ03"
);
assert_eq!(
format_tokenization_tag(true, "ABC").unwrap(),
"0REC0TRKABC|0FNZ03"
);
assert_eq!(
format_tokenization_tag(false, ""),
Err(Ecr17Error::ContractCodeInvalid)
);
assert_eq!(
format_tokenization_tag(false, &"x".repeat(19)),
Err(Ecr17Error::ContractCodeInvalid)
);
assert_eq!(
format_tokenization_tag(false, "AB|CD"),
Err(Ecr17Error::ContractCodeInvalid)
);
assert_eq!(
format_tokenization_tag(true, "ab cd"),
Err(Ecr17Error::ContractCodeInvalid)
);
}
#[test]
fn incremental_rejects_oversized_pre_auth_code() {
assert!(matches!(
build_incremental(T, C, 100, "1234567890", false, ""), Err(Ecr17Error::FieldOverflow { .. })
));
}
#[test]
fn builders_reject_invalid_payment_type() {
assert_eq!(
build_payment(T, C, 100, '9', false, false, ""),
Err(Ecr17Error::InvalidPaymentType { value: '9' })
);
assert_eq!(
build_card_verification(T, C, 'x', false),
Err(Ecr17Error::InvalidPaymentType { value: 'x' })
);
for d in ['0', '1', '2', '3'] {
assert!(build_payment(T, C, 100, d, false, false, "").is_ok());
assert!(build_card_verification(T, C, d, false).is_ok());
}
}
#[test]
fn pre_auth_rejects_negative_amount() {
assert_eq!(
build_pre_auth(T, C, -1, '0', false, false, ""),
Err(Ecr17Error::NegativeAmount)
);
}
}