Skip to content

Commit

Permalink
Add feig payment terminal code (#15)
Browse files Browse the repository at this point in the history
* Add feig payment terminal code

* fix test

* fix

* update metadat

* change dep

* Add README

---------

Co-authored-by: Dima Dorezyuk <[email protected]>
  • Loading branch information
dorezyuk and Dima Dorezyuk authored Feb 7, 2024
1 parent ca8c870 commit 843ecf4
Show file tree
Hide file tree
Showing 10 changed files with 2,132 additions and 18 deletions.
894 changes: 876 additions & 18 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"zvt_builder",
"zvt_cli",
"zvt_derive",
"zvt_feig_terminal",
]

[workspace.package]
Expand Down
1 change: 1 addition & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ crates_repository(
"//zvt_builder:Cargo.toml",
"//zvt_cli:Cargo.toml",
"//zvt_derive:Cargo.toml",
"//zvt_feig_terminal:Cargo.toml",
],
)

Expand Down
17 changes: 17 additions & 0 deletions zvt_feig_terminal/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
load("@crate_index//:defs.bzl", "all_crate_deps")

rust_library(
name = "zvt_feig_terminal",
srcs = glob(["src/*.rs"]),
deps = all_crate_deps() + ["//zvt"],
proc_macro_deps = all_crate_deps(proc_macro = True),
edition = "2021",
visibility = ["//visibility:public"],
)

rust_test(
name = "zvt_feig_terminal_test",
deps = all_crate_deps(normal_dev = True),
crate = ":zvt_feig_terminal",
)
31 changes: 31 additions & 0 deletions zvt_feig_terminal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "zvt_feig_terminal"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
zvt = { version = "0.1.0", path = "../zvt" }
anyhow = "1.0.75"
tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread", "net", "sync"] }
log = "0.4.20"
trust-dns-client = "0.23.0"
trust-dns-proto = "0.23.0"
async-trait = "0.1.73"
env_logger = "0.10.0"
tonic = "0.8.3"
tokio-stream = "0.1.14"
serde = { version = "1.0.188", features = ["derive"] }
thiserror = "1.0.48"
time = { version = "0.3.28", features = ["macros"] }
time-macros = "0.2.14"
pin-project = "1.1.3"
futures = "0.3.28"
async-stream = "0.3.5"
mockall = "0.11.4"
mockall_double = "0.3.0"
num-traits = "0.2.17"

[dev-dependencies]
serde_json = "1.0.105"
9 changes: 9 additions & 0 deletions zvt_feig_terminal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# ZVT Feig Terminal

The crate implements the application logic for using a Feig terminal in production.

We assume that you interact with the Feig terminal over TCP/IP.

The high level interface for interacting with the Feig terminal is implemented
in [src/feig.rs](src/feig.rs). It provides good defaults for reading cards and
allows you to begin, commit or cancel transactions.
137 changes: 137 additions & 0 deletions zvt_feig_terminal/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use anyhow::{bail, Result};
use serde::Deserialize;
use std::net::Ipv4Addr;

/// The config for the Feig terminal included in the
/// [PoleConfiguration::configuration].
#[derive(serde::Deserialize, PartialEq, Debug, Clone)]
pub struct FeigConfig {
/// The currency code as defined by ISO 4217. See
/// https://en.wikipedia.org/wiki/ISO_4217.
///
/// The input is the string representation of the currency as defined by
/// ISO 4217, e.x. `EUR` or `GBP`.
#[serde(default = "currency")]
#[serde(deserialize_with = "deserialize_iso_4217")]
pub currency: usize,

/// The pre-authorization amount in the smallest currency unit (e.x. Cent).
#[serde(default = "pre_authorization_amount")]
pub pre_authorization_amount: usize,

/// The default time to wait for reading a card in seconds. While a card is being read, the
/// payment terminal cannot do anything else (like refunding a transaction for example).
#[serde(default = "read_card_timeout")]
pub read_card_timeout: u8,

/// The password to the payment terminal.
#[serde(default)]
pub password: usize,
}

/// Deserializer which consumes a string code and returns the numerical code.
fn deserialize_iso_4217<'de, D>(deserializer: D) -> std::result::Result<usize, D::Error>
where
D: serde::Deserializer<'de>,
{
let code = String::deserialize(deserializer)?;
iso_4217(&code).map_err(serde::de::Error::custom)
}

/// The default currency (returns the EUR code).
const fn currency() -> usize {
978
}

/// The default read card timeout in seconds
const fn read_card_timeout() -> u8 {
15
}

/// The default pre-authorization amount in Cent (returns 25 EUR).
const fn pre_authorization_amount() -> usize {
2500
}

impl Default for FeigConfig {
fn default() -> Self {
Self {
currency: currency(),
pre_authorization_amount: pre_authorization_amount(),
read_card_timeout: read_card_timeout(),
password: 0,
}
}
}

/// Maps the currency code (three letters) to a numeric value.
///
/// The mapping is defined under the ISO 4217. See
/// https://en.wikipedia.org/wiki/ISO_4217
fn iso_4217(code: &str) -> Result<usize> {
match code.to_uppercase().as_str() {
// Keep the list sorted by the numeric value.
"SEK" => Ok(752),
"GBP" => Ok(826),
"EUR" => Ok(978),
_ => bail!("Unknown currency code {code}"),
}
}

/// The configuration needed for the entire payment terminal, which contains
/// parsed data.
#[derive(Clone, Debug)]
pub struct Config {
pub terminal_id: String,
/// We only use feig_serial to make sure we are connected to the proper
/// terminal.
pub feig_serial: String,
pub ip_address: Ipv4Addr,
/// Parsed from [PoleConfiguration::configuration].
pub feig_config: FeigConfig,
/// Maximum number of concurrent transactions.
pub transactions_max_num: usize,
}

impl Default for Config {
fn default() -> Self {
Self {
terminal_id: String::default(),
feig_serial: String::default(),
ip_address: Ipv4Addr::new(0, 0, 0, 0),
feig_config: FeigConfig::default(),
transactions_max_num: 1,
}
}
}

#[cfg(test)]
mod tests {

use super::*;

#[test]
fn test_feig_config() {
// Valid inputs.
let empty = serde_json::from_str::<FeigConfig>("{}").unwrap();
assert_eq!(empty, FeigConfig::default());

let with_currency = serde_json::from_str::<FeigConfig>("{\"currency\": \"GBP\"}").unwrap();
assert_eq!(with_currency.currency, 826);
assert_eq!(with_currency.pre_authorization_amount, 2500);

let with_all = serde_json::from_str::<FeigConfig>(
"{\"currency\": \"GBP\", \"pre_authorization_amount\": 10}",
)
.unwrap();
assert_eq!(with_all.currency, 826);
assert_eq!(with_all.pre_authorization_amount, 10);

// Invalid inputs.
assert!(serde_json::from_str::<FeigConfig>("{\"currency\": \"ABC\"}").is_err());
assert!(serde_json::from_str::<FeigConfig>("{\"currency\": 123}").is_err());
assert!(
serde_json::from_str::<FeigConfig>("{\"pre_authorization_amount\": \"AB\"}").is_err()
);
}
}
Loading

0 comments on commit 843ecf4

Please sign in to comment.