- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
1297 lines
46 KiB
Rust
1297 lines
46 KiB
Rust
//! Module for Feig's payment terminals.
|
|
//!
|
|
//! The module is an EVerest wrapper around the [ZVT](https://github.com/qwello/zvt)
|
|
//! crate. It talks directly to the underlying hardware and instructs it to read
|
|
//! cards.
|
|
//!
|
|
//! ## NFC card handling
|
|
//!
|
|
//! In case a NFC card is presented, the module will issue a token on the
|
|
//! `auth_token_provider` interface. The token-id will be generated from the
|
|
//! NFC card metadata.
|
|
//!
|
|
//! ## Bank card handling
|
|
//!
|
|
//! In case a bank card is presented, the module will also issue a token on the
|
|
//! `auth_token_provider` interface. The token-id must be provided by the
|
|
//! `bank_session_token` interface. In order to block the
|
|
//! `pre_authorization_amount`, the user must call the `auth_token_validator`
|
|
//! interface of the module. The modile commits (and releases the surplus from
|
|
//! the pre-authorized amount) once it receives the same a token-id on its
|
|
//! `session_cost` interface.
|
|
//!
|
|
//! ## Implementation details
|
|
//!
|
|
//! For more details checkout the
|
|
//! * [ZVT Rust implementation](https://github.com/qwello/zvt)
|
|
//! * [ZVT documentation](https://www.terminalhersteller.de/downloads.aspx)
|
|
//! * [Feig homepage](https://www.feig-payment.de/)
|
|
//!
|
|
#![allow(non_snake_case, non_camel_case_types, clippy::all)]
|
|
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
|
|
|
use anyhow::Result;
|
|
use generated::errors::payment_terminal::{Error as PTError, PaymentTerminalError};
|
|
use generated::types::{
|
|
authorization::{
|
|
AuthorizationStatus, AuthorizationType, IdToken, IdTokenType, ProvidedIdToken,
|
|
ValidationResult,
|
|
},
|
|
money::MoneyAmount,
|
|
payment_terminal::{BankSessionToken, BankTransactionSummary},
|
|
session_cost::{SessionCost, SessionStatus, TariffMessage},
|
|
};
|
|
use generated::{
|
|
AuthTokenProviderServiceSubscriber, AuthTokenValidatorServiceSubscriber,
|
|
BankSessionTokenProviderClientSubscriber, Context, Module, ModulePublisher, OnReadySubscriber,
|
|
PaymentTerminalServiceSubscriber, SessionCostClientSubscriber,
|
|
};
|
|
use std::collections::HashMap;
|
|
use std::hash::{Hash, Hasher};
|
|
use std::sync::{mpsc::channel, mpsc::Sender, Arc, Mutex};
|
|
use std::time::Duration;
|
|
use std::{net::Ipv4Addr, str::FromStr};
|
|
use zvt::constants::ErrorMessages;
|
|
use zvt_feig_terminal::config::{Config, FeigConfig};
|
|
use zvt_feig_terminal::feig::{CardInfo, Error};
|
|
|
|
const INVALID_BANK_TOKEN: &str = "PAYMENT_TERMINAL_INVALID";
|
|
|
|
mod backoff {
|
|
use std::cmp::min;
|
|
use std::time::{Duration, Instant};
|
|
|
|
pub struct Backoff {
|
|
next_retry: Instant,
|
|
backoff_secs: u64,
|
|
max_backoff_secs: u64,
|
|
}
|
|
|
|
impl Backoff {
|
|
pub fn from_secs(max_backoff_secs: u64) -> Self {
|
|
Self {
|
|
next_retry: Instant::now(),
|
|
backoff_secs: 1,
|
|
max_backoff_secs,
|
|
}
|
|
}
|
|
|
|
pub fn is_ready(&self) -> bool {
|
|
Instant::now() >= self.next_retry
|
|
}
|
|
|
|
pub fn record_failure(&mut self) {
|
|
self.backoff_secs = min(self.backoff_secs * 2, self.max_backoff_secs);
|
|
log::info!(
|
|
"Recorded failure: next retry will be in {}",
|
|
self.backoff_secs
|
|
);
|
|
self.next_retry = Instant::now() + Duration::from_secs(self.backoff_secs);
|
|
}
|
|
|
|
pub fn record_success(&mut self) {
|
|
self.backoff_secs = 1;
|
|
log::debug!("Next retry will be in {}", self.backoff_secs);
|
|
self.next_retry = Instant::now() + Duration::from_secs(self.backoff_secs);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn ready_immediately_after_creation() {
|
|
let b = Backoff::from_secs(60);
|
|
assert!(b.is_ready());
|
|
}
|
|
|
|
#[test]
|
|
fn not_ready_after_failure() {
|
|
let mut b = Backoff::from_secs(60);
|
|
b.record_failure();
|
|
assert!(!b.is_ready());
|
|
assert_eq!(b.backoff_secs, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn exponential_increase() {
|
|
let mut b = Backoff::from_secs(60);
|
|
b.record_failure();
|
|
assert_eq!(b.backoff_secs, 2);
|
|
b.record_failure();
|
|
assert_eq!(b.backoff_secs, 4);
|
|
b.record_failure();
|
|
assert_eq!(b.backoff_secs, 8);
|
|
}
|
|
|
|
#[test]
|
|
fn caps_at_max() {
|
|
let mut b = Backoff::from_secs(8);
|
|
for _ in 0..10 {
|
|
b.record_failure();
|
|
}
|
|
assert_eq!(b.backoff_secs, 8);
|
|
}
|
|
|
|
#[test]
|
|
fn success_resets() {
|
|
let mut b = Backoff::from_secs(60);
|
|
b.record_failure();
|
|
b.record_failure();
|
|
b.record_success();
|
|
assert_eq!(b.backoff_secs, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
mod sync_feig {
|
|
use anyhow::Result;
|
|
use std::sync::Mutex;
|
|
use zvt_feig_terminal::{
|
|
config::Config,
|
|
feig::{CardInfo, Feig, TransactionSummary},
|
|
};
|
|
|
|
pub struct SyncFeig {
|
|
/// Tokio runtime to call the Feig functions.
|
|
rt: tokio::runtime::Runtime,
|
|
|
|
/// The async impl of the Feig.
|
|
inner: Mutex<Feig>,
|
|
}
|
|
|
|
/// Sync interface for the Feig.
|
|
///
|
|
/// The `Feig` implements an async interface, which we can't use in EVerest.
|
|
/// Here we wrap the async functions and expose the sync version of them.
|
|
///
|
|
/// Below we allow `dead_code` so we just wrap all async functions even
|
|
/// though they may be unused.
|
|
#[allow(dead_code)]
|
|
#[cfg_attr(test, mockall::automock)]
|
|
impl SyncFeig {
|
|
pub fn new(config: Config) -> Self {
|
|
// Create a runtime for the Feig terminal.
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
.max_blocking_threads(1)
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
|
|
// Create the Feig terminal itself.
|
|
let feig = rt.block_on(async {
|
|
loop {
|
|
let response = Feig::new(config.clone()).await;
|
|
match response {
|
|
Ok(inner) => {
|
|
log::info!("Payment terminal initialized.");
|
|
return inner;
|
|
}
|
|
Err(e) => {
|
|
log::warn!("Payment terminal not initialized {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Self {
|
|
rt,
|
|
inner: Mutex::new(feig),
|
|
}
|
|
}
|
|
|
|
pub fn read_card(&self) -> Result<CardInfo> {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
self.rt.block_on(inner.read_card())
|
|
}
|
|
|
|
pub fn begin_transaction(&self, token: &str, amount: usize) -> Result<()> {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
self.rt.block_on(inner.begin_transaction(token, amount))
|
|
}
|
|
|
|
pub fn cancel_transaction(&self, token: &str) -> Result<()> {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
self.rt.block_on(inner.cancel_transaction(token))
|
|
}
|
|
|
|
pub fn commit_transaction(&self, token: &str, amount: u64) -> Result<TransactionSummary> {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
self.rt.block_on(inner.commit_transaction(token, amount))
|
|
}
|
|
|
|
pub fn configure(&self) -> Result<()> {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
self.rt.block_on(inner.configure())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[mockall_double::double]
|
|
use sync_feig::SyncFeig;
|
|
|
|
impl ProvidedIdToken {
|
|
fn new(
|
|
id_token: String,
|
|
authorization_type: AuthorizationType,
|
|
connectors: Option<Vec<i64>>,
|
|
) -> Self {
|
|
Self {
|
|
parent_id_token: None,
|
|
id_token: IdToken {
|
|
value: id_token,
|
|
r#type: IdTokenType::Local,
|
|
additional_info: None,
|
|
},
|
|
authorization_type,
|
|
certificate: None,
|
|
connectors,
|
|
iso_15118_certificate_hash_data: None,
|
|
prevalidated: None,
|
|
request_id: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<anyhow::Error> for PTError {
|
|
fn from(value: anyhow::Error) -> Self {
|
|
match value.downcast_ref::<Error>() {
|
|
Some(inner) => match inner {
|
|
Error::TidMismatch => {
|
|
PTError::PaymentTerminal(PaymentTerminalError::TerminalIdNotSet)
|
|
}
|
|
Error::IncorrectDeviceId { .. } => {
|
|
PTError::PaymentTerminal(PaymentTerminalError::IncorrectDeviceId)
|
|
}
|
|
_ => PTError::PaymentTerminal(PaymentTerminalError::GenericPaymentTerminalError),
|
|
},
|
|
None => PTError::PaymentTerminal(PaymentTerminalError::GenericPaymentTerminalError),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<ErrorMessages> for AuthorizationStatus {
|
|
fn from(code: ErrorMessages) -> Self {
|
|
match code {
|
|
#[cfg(feature = "with_lavego_error_codes")]
|
|
ErrorMessages::ContactlessTransactionCountExceeded => AuthorizationStatus::PinRequired,
|
|
#[cfg(feature = "with_lavego_error_codes")]
|
|
ErrorMessages::PinEntryRequiredx33 => AuthorizationStatus::PinRequired,
|
|
#[cfg(feature = "with_lavego_error_codes")]
|
|
ErrorMessages::PinEntryRequiredx3d => AuthorizationStatus::PinRequired,
|
|
#[cfg(feature = "with_lavego_error_codes")]
|
|
ErrorMessages::PinEntryRequiredx41 => AuthorizationStatus::PinRequired,
|
|
ErrorMessages::PinProcessingNotPossible
|
|
| ErrorMessages::NecessaryDeviceNotPresentOrDefective => AuthorizationStatus::PinRequired,
|
|
ErrorMessages::CreditNotSufficient => AuthorizationStatus::NoCredit,
|
|
ErrorMessages::PaymentMethodNotSupported => AuthorizationStatus::Blocked,
|
|
ErrorMessages::AbortViaTimeoutOrAbortKey => AuthorizationStatus::Invalid,
|
|
#[cfg(feature = "with_lavego_error_codes")]
|
|
ErrorMessages::Declined => AuthorizationStatus::Timeout,
|
|
ErrorMessages::ReceiverNotReady
|
|
| ErrorMessages::SystemError
|
|
| ErrorMessages::ErrorFromDialUp // error from dial-up/communication fault
|
|
=> AuthorizationStatus::Timeout,
|
|
_ => AuthorizationStatus::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Hash for AuthorizationType {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
std::mem::discriminant(self).hash(state);
|
|
}
|
|
}
|
|
|
|
impl Eq for AuthorizationType {}
|
|
|
|
impl From<AuthorizationStatus> for ValidationResult {
|
|
fn from(value: AuthorizationStatus) -> Self {
|
|
ValidationResult {
|
|
authorization_status: value,
|
|
tariff_messages: Vec::new(),
|
|
allowed_energy_transfer_modes: None,
|
|
certificate_status: None,
|
|
evse_ids: None,
|
|
expiry_time: None,
|
|
parent_id_token: None,
|
|
reservation_id: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the default card type to connector mapping where all card types
|
|
/// are enabled for all connectors.
|
|
fn default_card_type_to_connector() -> HashMap<AuthorizationType, Option<Vec<i64>>> {
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, None),
|
|
(AuthorizationType::RFID, None),
|
|
])
|
|
}
|
|
|
|
/// Main struct for this module.
|
|
pub struct PaymentTerminalModule {
|
|
/// Sender for the `ModulePublisher` -> to get the publisher from `on_ready`
|
|
/// into the main thread.
|
|
tx: Sender<ModulePublisher>,
|
|
|
|
/// The Feig interface.
|
|
feig: SyncFeig,
|
|
|
|
/// The mapping of supported connectors for every card type. None means
|
|
/// no restrictions, Some(vec![]) means no connector.
|
|
card_type_to_connector: Mutex<HashMap<AuthorizationType, Option<Vec<i64>>>>,
|
|
|
|
/// The configurable pre-auth.
|
|
pre_authorization_amount: Mutex<usize>,
|
|
}
|
|
|
|
impl PaymentTerminalModule {
|
|
/// Waits for a card and generates an auth token.
|
|
///
|
|
/// Regardless of the card type we don't flag the token as pre-validated to
|
|
/// allow the consumers to add custom validation steps on top. For bank
|
|
/// cards use the `auth_token_validator` to pre-authorize money.
|
|
fn read_card(&self, publishers: &ModulePublisher) -> Result<()> {
|
|
let mut token: Option<String> = None;
|
|
|
|
// Wait for the card.
|
|
let mut read_card_loop = || -> CardInfo {
|
|
// Long backoff to the payment provider backend to not overload it.
|
|
let mut configure_backoff = backoff::Backoff::from_secs(3600);
|
|
let mut bank_token_backoff = backoff::Backoff::from_secs(60);
|
|
loop {
|
|
if configure_backoff.is_ready() {
|
|
if let Err(inner) = self.feig.configure() {
|
|
log::warn!("Failed to configure: {inner:?}");
|
|
let inner: PTError = inner.into();
|
|
publishers.payment_terminal.raise_error(inner.into());
|
|
configure_backoff.record_failure();
|
|
} else {
|
|
configure_backoff.record_success();
|
|
publishers.payment_terminal.clear_all_errors();
|
|
}
|
|
}
|
|
|
|
let bank_cards_enabled = {
|
|
let mut map_guard = self.card_type_to_connector.lock().unwrap();
|
|
map_guard
|
|
.entry(AuthorizationType::BankCard)
|
|
.or_default()
|
|
.as_ref()
|
|
.map_or(true, |v| !v.is_empty())
|
|
};
|
|
|
|
// Attempting to get an invoice token
|
|
if token.is_none() && bank_cards_enabled {
|
|
if let Some(publisher) = publishers.bank_session_token_slots.get(0) {
|
|
if bank_token_backoff.is_ready() {
|
|
token = publisher.get_bank_session_token().map_or(None, |v| v.token);
|
|
|
|
if token.is_none() {
|
|
bank_token_backoff.record_failure();
|
|
} else {
|
|
log::info!("Received the invoice token {token:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
match self.feig.read_card() {
|
|
Ok(card_info) => return card_info,
|
|
Err(e) => {
|
|
if let Some(Error::NoCardPresented) = e.downcast_ref::<Error>() {
|
|
log::debug!("No card presented");
|
|
} else {
|
|
log::warn!("Failed to read a card {e:?}");
|
|
// Cleared in the next loop if we can configure the
|
|
// feig.
|
|
let inner: PTError = e.into();
|
|
publishers.payment_terminal.raise_error(inner.into());
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
let card_info = read_card_loop();
|
|
let mut map_guard = self.card_type_to_connector.lock().unwrap();
|
|
|
|
let provided_token = match card_info {
|
|
CardInfo::Bank => ProvidedIdToken::new(
|
|
// If we don't have a valid token, still issue the token but
|
|
// reject it in the validation - so e.x. display can still
|
|
// react to cards being read.
|
|
token.unwrap_or(INVALID_BANK_TOKEN.to_string()),
|
|
AuthorizationType::BankCard,
|
|
map_guard
|
|
.entry(AuthorizationType::BankCard)
|
|
.or_default()
|
|
.clone(),
|
|
),
|
|
CardInfo::MembershipCard(id_token) => ProvidedIdToken::new(
|
|
id_token,
|
|
AuthorizationType::RFID,
|
|
map_guard
|
|
.entry(AuthorizationType::RFID)
|
|
.or_default()
|
|
.clone(),
|
|
),
|
|
};
|
|
publishers.token_provider.provided_token(provided_token)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// The implementation of the `SessionCostClientSubscriber::on_session_cost`,
|
|
/// but here we can return errors.
|
|
fn on_session_cost_impl(&self, context: &Context, value: SessionCost) -> Result<()> {
|
|
let Some(id_tag) = value.id_tag else {
|
|
return Ok(());
|
|
};
|
|
|
|
// We only care about bank cards.
|
|
match id_tag.authorization_type {
|
|
AuthorizationType::BankCard => (),
|
|
_ => return Ok(()),
|
|
}
|
|
|
|
if let SessionStatus::Running = value.status {
|
|
log::info!("The session is still running");
|
|
return Ok(());
|
|
}
|
|
|
|
let total_cost = value
|
|
.cost_chunks
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.fold(0, |acc, chunk| {
|
|
acc + chunk.cost.unwrap_or(MoneyAmount { value: 0 }).value
|
|
});
|
|
|
|
let res = self
|
|
.feig
|
|
.commit_transaction(&id_tag.id_token.value, total_cost as u64)?;
|
|
|
|
context
|
|
.publisher
|
|
.payment_terminal
|
|
.bank_transaction_summary(BankTransactionSummary {
|
|
session_token: Some(BankSessionToken {
|
|
token: Some(id_tag.id_token.value.clone()),
|
|
}),
|
|
transaction_data: Some(format!("{:06}", res.trace_number.unwrap_or_default())),
|
|
})?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl AuthTokenProviderServiceSubscriber for PaymentTerminalModule {}
|
|
|
|
impl BankSessionTokenProviderClientSubscriber for PaymentTerminalModule {}
|
|
|
|
impl OnReadySubscriber for PaymentTerminalModule {
|
|
fn on_ready(&self, publishers: &ModulePublisher) {
|
|
// Send the publishers to the main thread.
|
|
self.tx.send(publishers.clone()).unwrap();
|
|
}
|
|
}
|
|
|
|
impl SessionCostClientSubscriber for PaymentTerminalModule {
|
|
fn on_session_cost(&self, context: &Context, value: SessionCost) {
|
|
let res = self.on_session_cost_impl(context, value);
|
|
match res {
|
|
Ok(_) => log::debug!("Transaction successful"),
|
|
Err(err) => log::error!("Transaction failed {err:}"),
|
|
}
|
|
}
|
|
|
|
fn on_tariff_message(&self, _context: &Context, value: TariffMessage) {
|
|
if let Some(pre_authorization_amount) = value.pre_authorized_amount {
|
|
log::info!(
|
|
"Updating pre_authorization_amount to {}",
|
|
pre_authorization_amount.value
|
|
);
|
|
*self.pre_authorization_amount.lock().unwrap() =
|
|
pre_authorization_amount.value as usize;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AuthTokenValidatorServiceSubscriber for PaymentTerminalModule {
|
|
fn validate_token(
|
|
&self,
|
|
_context: &Context,
|
|
provided_token: ProvidedIdToken,
|
|
) -> ::everestrs::Result<ValidationResult> {
|
|
if provided_token.authorization_type != AuthorizationType::BankCard {
|
|
log::warn!(
|
|
"{:?} not supported: can only validate `AuthorizationType::BankCard`",
|
|
provided_token.authorization_type
|
|
);
|
|
return Ok(AuthorizationStatus::Invalid.into());
|
|
}
|
|
|
|
if &provided_token.id_token.value == INVALID_BANK_TOKEN {
|
|
log::warn!("Validating a `BankCard` without an invoice token");
|
|
return Ok(AuthorizationStatus::Invalid.into());
|
|
}
|
|
|
|
if let Err(err) = self.feig.begin_transaction(
|
|
&provided_token.id_token.value,
|
|
*self.pre_authorization_amount.lock().unwrap(),
|
|
) {
|
|
log::warn!("Failed to start a transaction: {err:?}");
|
|
match err.downcast_ref::<ErrorMessages>() {
|
|
Some(rejection_reason) => {
|
|
log::info!("Recieved rejection reason {}", rejection_reason);
|
|
let status: AuthorizationStatus = (*rejection_reason).into();
|
|
return Ok(status.into());
|
|
}
|
|
None => {
|
|
log::info!("No error code provided");
|
|
return Ok(AuthorizationStatus::Invalid.into());
|
|
}
|
|
};
|
|
}
|
|
|
|
Ok(AuthorizationStatus::Accepted.into())
|
|
}
|
|
}
|
|
|
|
impl PaymentTerminalServiceSubscriber for PaymentTerminalModule {
|
|
fn enable_card_reading(
|
|
&self,
|
|
_context: &Context,
|
|
connector_id: i64,
|
|
supported_cards: Vec<AuthorizationType>,
|
|
) -> ::everestrs::Result<()> {
|
|
let mut map_guard = self.card_type_to_connector.lock().unwrap();
|
|
|
|
for (card_type, connector_ids) in map_guard.iter_mut() {
|
|
match supported_cards.iter().find(|card| *card == card_type) {
|
|
Some(_) => {
|
|
// Card is allowed -> Add the connector under the card type
|
|
let connector_ids = connector_ids.get_or_insert_default();
|
|
if let Err(idx) = connector_ids.binary_search(&connector_id) {
|
|
connector_ids.insert(idx, connector_id);
|
|
}
|
|
}
|
|
None => {
|
|
// Card is forbidden -> remove the connector from the card type
|
|
let connector_ids = connector_ids.get_or_insert_default();
|
|
if let Ok(idx) = connector_ids.binary_search(&connector_id) {
|
|
connector_ids.remove(idx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn allow_all_cards_for_every_connector(&self, _context: &Context) -> ::everestrs::Result<()> {
|
|
let mut map_guard = self.card_type_to_connector.lock().unwrap();
|
|
*map_guard = default_card_type_to_connector();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[everestrs::main]
|
|
fn main(module: &Module) -> Result<()> {
|
|
let config = module.get_config();
|
|
log::info!("Received the config {config:?}");
|
|
|
|
let pt_config = Config {
|
|
terminal_id: config.terminal_id,
|
|
feig_serial: config.feig_serial,
|
|
ip_address: Ipv4Addr::from_str(&config.ip)?,
|
|
feig_config: FeigConfig {
|
|
currency: config.currency as usize,
|
|
read_card_timeout: config.read_card_timeout as u8,
|
|
password: config.password as usize,
|
|
end_of_day_max_interval: config.end_of_day_max_interval as u64,
|
|
},
|
|
transactions_max_num: config.transactions_max_num as usize,
|
|
};
|
|
|
|
let (tx, rx) = channel();
|
|
|
|
let pt_module = Arc::new(PaymentTerminalModule {
|
|
tx,
|
|
feig: SyncFeig::new(pt_config),
|
|
card_type_to_connector: Mutex::new(default_card_type_to_connector()),
|
|
pre_authorization_amount: Mutex::new(config.pre_authorization_amount as usize),
|
|
});
|
|
|
|
let _publishers = module.start(
|
|
pt_module.clone(),
|
|
pt_module.clone(),
|
|
pt_module.clone(),
|
|
pt_module.clone(),
|
|
|_index| pt_module.clone(),
|
|
|_index| pt_module.clone(),
|
|
);
|
|
|
|
let read_card_debounce = Duration::from_secs(config.read_card_debounce as u64);
|
|
let publishers = rx.recv()?;
|
|
loop {
|
|
log::debug!("Waiting for transactions...");
|
|
let res = pt_module.read_card(&publishers);
|
|
match res {
|
|
Ok(()) => {
|
|
log::info!("Started a transaction");
|
|
std::thread::sleep(read_card_debounce);
|
|
}
|
|
Err(err) => log::error!("Failed to start a transaction {err:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use self::generated::types::money::Currency;
|
|
use self::generated::types::money::CurrencyCode;
|
|
use self::generated::types::session_cost::SessionCostChunk;
|
|
use self::generated::BankSessionTokenProviderClientPublisher;
|
|
|
|
use super::*;
|
|
use mockall::predicate::eq;
|
|
use zvt_feig_terminal::feig::TransactionSummary;
|
|
|
|
impl From<SyncFeig> for PaymentTerminalModule {
|
|
fn from(feig_mock: SyncFeig) -> Self {
|
|
let (tx, _) = channel();
|
|
|
|
PaymentTerminalModule {
|
|
tx,
|
|
feig: feig_mock,
|
|
card_type_to_connector: Mutex::new(default_card_type_to_connector()),
|
|
pre_authorization_amount: Mutex::new(11),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a ModulePublisher> for Context<'a> {
|
|
fn from(everest_mock: &'a ModulePublisher) -> Context<'a> {
|
|
Context {
|
|
name: "foo",
|
|
publisher: &everest_mock,
|
|
index: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn payment_terminal__read_card__get_bank_session_token_failed() {
|
|
let PARAMETERS = [
|
|
(
|
|
CardInfo::Bank,
|
|
AuthorizationType::BankCard,
|
|
INVALID_BANK_TOKEN,
|
|
),
|
|
(
|
|
CardInfo::MembershipCard("some id".to_string()),
|
|
AuthorizationType::RFID,
|
|
"some id",
|
|
),
|
|
];
|
|
|
|
for (card_info, authorization_type, id_token) in PARAMETERS {
|
|
let mut feig_mock = SyncFeig::default();
|
|
feig_mock.expect_configure().times(1).return_once(|| Ok(()));
|
|
feig_mock
|
|
.expect_read_card()
|
|
.times(1)
|
|
.return_once(|| Ok(card_info));
|
|
|
|
let mut everest_mock = ModulePublisher::default();
|
|
everest_mock
|
|
.bank_session_token_slots
|
|
.push(BankSessionTokenProviderClientPublisher::default());
|
|
|
|
everest_mock.bank_session_token_slots[0]
|
|
.expect_get_bank_session_token()
|
|
.times(1)
|
|
.return_once(|| Err(::everestrs::Error::HandlerException("oh no".to_string())));
|
|
|
|
everest_mock
|
|
.token_provider
|
|
.expect_provided_token()
|
|
.times(1)
|
|
.withf(move |arg| {
|
|
&arg.id_token.value == id_token && arg.authorization_type == authorization_type
|
|
})
|
|
.return_once(|_| Ok(()));
|
|
|
|
everest_mock
|
|
.payment_terminal
|
|
.expect_clear_all_errors()
|
|
.times(1)
|
|
.return_once(|| ());
|
|
|
|
let pt_module: PaymentTerminalModule = feig_mock.into();
|
|
assert!(pt_module.read_card(&everest_mock).is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn payment_terminal__read_card__no_bank_session_token() {
|
|
let PARAMETERS = [
|
|
(
|
|
CardInfo::Bank,
|
|
AuthorizationType::BankCard,
|
|
INVALID_BANK_TOKEN,
|
|
),
|
|
(
|
|
CardInfo::MembershipCard("some id".to_string()),
|
|
AuthorizationType::RFID,
|
|
"some id",
|
|
),
|
|
];
|
|
// Here we don't have a bank session provider defined.
|
|
for (card_info, authorization_type, id_token) in PARAMETERS {
|
|
let mut feig_mock = SyncFeig::default();
|
|
feig_mock.expect_configure().times(1).return_once(|| Ok(()));
|
|
feig_mock
|
|
.expect_read_card()
|
|
.times(1)
|
|
.return_once(|| Ok(card_info));
|
|
|
|
let mut everest_mock = ModulePublisher::default();
|
|
|
|
everest_mock
|
|
.token_provider
|
|
.expect_provided_token()
|
|
.times(1)
|
|
.withf(move |arg| {
|
|
&arg.id_token.value == id_token && arg.authorization_type == authorization_type
|
|
})
|
|
.return_once(|_| Ok(()));
|
|
|
|
everest_mock
|
|
.payment_terminal
|
|
.expect_clear_all_errors()
|
|
.times(1)
|
|
.return_once(|| ());
|
|
|
|
let pt_module: PaymentTerminalModule = feig_mock.into();
|
|
assert!(pt_module.read_card(&everest_mock).is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Test that bank cards are not processed when bank cards are disabled
|
|
fn payment_terminal__read_card__bank_cards_disabled() {
|
|
let mut feig_mock = SyncFeig::default();
|
|
feig_mock.expect_configure().times(1).return_once(|| Ok(()));
|
|
feig_mock
|
|
.expect_read_card()
|
|
.times(1)
|
|
.return_once(|| Ok(CardInfo::Bank));
|
|
|
|
let mut everest_mock = ModulePublisher::default();
|
|
everest_mock
|
|
.bank_session_token_slots
|
|
.push(BankSessionTokenProviderClientPublisher::default());
|
|
|
|
// get_bank_session_token should NOT be called when bank cards are disabled
|
|
everest_mock.bank_session_token_slots[0]
|
|
.expect_get_bank_session_token()
|
|
.times(0);
|
|
|
|
everest_mock
|
|
.token_provider
|
|
.expect_provided_token()
|
|
.times(1)
|
|
.withf(|arg| {
|
|
// When bank cards are disabled, we should get INVALID_BANK_TOKEN
|
|
&arg.id_token.value == INVALID_BANK_TOKEN
|
|
&& arg.authorization_type == AuthorizationType::BankCard
|
|
// Connectors should be empty (bank cards disabled)
|
|
&& arg.connectors == Some(vec![])
|
|
})
|
|
.return_once(|_| Ok(()));
|
|
|
|
everest_mock
|
|
.payment_terminal
|
|
.expect_clear_all_errors()
|
|
.times(1)
|
|
.return_once(|| ());
|
|
|
|
let (tx, _) = channel();
|
|
// Set up module with bank cards disabled (empty connector list)
|
|
let pt_module = PaymentTerminalModule {
|
|
tx,
|
|
feig: feig_mock,
|
|
card_type_to_connector: Mutex::new(HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![])), // Empty = disabled
|
|
(AuthorizationType::RFID, None),
|
|
])),
|
|
pre_authorization_amount: Mutex::new(11),
|
|
};
|
|
|
|
assert!(pt_module.read_card(&everest_mock).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn payment_terminal__read_card__success() {
|
|
let PARAMETERS = [
|
|
(
|
|
CardInfo::Bank,
|
|
AuthorizationType::BankCard,
|
|
"some bank token",
|
|
"some bank token",
|
|
),
|
|
(
|
|
CardInfo::MembershipCard("some id".to_string()),
|
|
AuthorizationType::RFID,
|
|
"some bank token",
|
|
"some id",
|
|
),
|
|
];
|
|
|
|
for (card_info, authorization_type, bank_token, id_token) in PARAMETERS {
|
|
// When the token is None, we still expect membership card to work
|
|
let mut feig_mock = SyncFeig::default();
|
|
|
|
feig_mock
|
|
.expect_read_card()
|
|
.times(1)
|
|
.return_once(|| Ok(card_info));
|
|
|
|
let mut everest_mock = ModulePublisher::default();
|
|
everest_mock
|
|
.bank_session_token_slots
|
|
.push(BankSessionTokenProviderClientPublisher::default());
|
|
|
|
everest_mock.bank_session_token_slots[0]
|
|
.expect_get_bank_session_token()
|
|
.times(1)
|
|
.return_once(|| {
|
|
Ok(BankSessionToken {
|
|
token: Some(bank_token.to_owned()),
|
|
})
|
|
});
|
|
|
|
everest_mock
|
|
.token_provider
|
|
.expect_provided_token()
|
|
.times(1)
|
|
.withf(move |arg| {
|
|
&arg.id_token.value == id_token && arg.authorization_type == authorization_type
|
|
})
|
|
.return_once(|_| Ok(()));
|
|
|
|
everest_mock
|
|
.payment_terminal
|
|
.expect_clear_all_errors()
|
|
.times(1)
|
|
.return_once(|| ());
|
|
|
|
feig_mock.expect_configure().times(1).return_once(|| Ok(()));
|
|
|
|
let pt_module: PaymentTerminalModule = feig_mock.into();
|
|
|
|
assert!(pt_module.read_card(&everest_mock).is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// We test that we don't commit anything for inputs which should be ignored.
|
|
fn payment_terminal__on_session_cost_impl__noop() {
|
|
let parameters = [
|
|
(AuthorizationType::OCPP, SessionStatus::Finished),
|
|
(AuthorizationType::RFID, SessionStatus::Finished),
|
|
(AuthorizationType::BankCard, SessionStatus::Running),
|
|
];
|
|
|
|
for (auth_type, status) in parameters {
|
|
let session_cost = SessionCost {
|
|
cost_chunks: None,
|
|
currency: Currency {
|
|
code: Some(CurrencyCode::EUR),
|
|
decimals: None,
|
|
},
|
|
id_tag: Some(ProvidedIdToken::new(String::new(), auth_type, None)),
|
|
status,
|
|
session_id: String::new(),
|
|
idle_price: None,
|
|
charging_price: None,
|
|
next_period: None,
|
|
message: None,
|
|
qr_code: None,
|
|
};
|
|
|
|
let everest_mock = ModulePublisher::default();
|
|
let feig = SyncFeig::default();
|
|
let pt_module: PaymentTerminalModule = feig.into();
|
|
|
|
assert!(pt_module
|
|
.on_session_cost_impl(&(&everest_mock).into(), session_cost)
|
|
.is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// We test that we commit the right amount for transactions which are for
|
|
/// us.
|
|
fn payment_terminal__on_session_cost_impl() {
|
|
let parameters = [
|
|
(None, 0),
|
|
(Some(vec![]), 0),
|
|
(Some(vec![None]), 0),
|
|
(Some(vec![Some(1), Some(2)]), 3),
|
|
];
|
|
|
|
for (cost_chunks, amount) in parameters {
|
|
let session_cost = SessionCost {
|
|
cost_chunks: cost_chunks.map(|chunks| {
|
|
chunks
|
|
.into_iter()
|
|
.map(|cost| SessionCostChunk {
|
|
category: None,
|
|
cost: cost.map(|value| MoneyAmount { value }),
|
|
timestamp_from: None,
|
|
timestamp_to: None,
|
|
metervalue_from: None,
|
|
metervalue_to: None,
|
|
})
|
|
.collect()
|
|
}),
|
|
currency: Currency {
|
|
code: Some(CurrencyCode::EUR),
|
|
decimals: None,
|
|
},
|
|
id_tag: Some(ProvidedIdToken::new(
|
|
"token".to_string(),
|
|
AuthorizationType::BankCard,
|
|
None,
|
|
)),
|
|
status: SessionStatus::Finished,
|
|
session_id: String::new(),
|
|
idle_price: None,
|
|
charging_price: None,
|
|
next_period: None,
|
|
message: None,
|
|
qr_code: None,
|
|
};
|
|
let mut everest_mock = ModulePublisher::default();
|
|
everest_mock
|
|
.payment_terminal
|
|
.expect_bank_transaction_summary()
|
|
.times(1)
|
|
.returning(|_| Ok(()));
|
|
|
|
let mut feig = SyncFeig::default();
|
|
feig.expect_commit_transaction()
|
|
.times(1)
|
|
.with(eq("token"), eq(amount))
|
|
.returning(|_, _| {
|
|
Ok(TransactionSummary {
|
|
terminal_id: None,
|
|
amount: None,
|
|
trace_number: None,
|
|
date: None,
|
|
time: None,
|
|
})
|
|
});
|
|
let pt_module: PaymentTerminalModule = feig.into();
|
|
|
|
assert!(pt_module
|
|
.on_session_cost_impl(&(&everest_mock).into(), session_cost)
|
|
.is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Test validate_token with non-BankCard authorization type
|
|
fn payment_terminal__validate_token__invalid_authorization_type() {
|
|
let parameters = [AuthorizationType::OCPP, AuthorizationType::RFID];
|
|
|
|
for auth_type in parameters {
|
|
let feig_mock = SyncFeig::default();
|
|
let pt_module: PaymentTerminalModule = feig_mock.into();
|
|
|
|
let provided_token = ProvidedIdToken::new("some_token".to_string(), auth_type, None);
|
|
let everest_mock = ModulePublisher::default();
|
|
|
|
let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
|
|
assert!(result.is_ok());
|
|
let validation_result = result.unwrap();
|
|
assert_eq!(
|
|
validation_result.authorization_status,
|
|
AuthorizationStatus::Invalid
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Test validate_token with INVALID_BANK_TOKEN
|
|
fn payment_terminal__validate_token__invalid_bank_token() {
|
|
let feig_mock = SyncFeig::default();
|
|
let pt_module: PaymentTerminalModule = feig_mock.into();
|
|
|
|
let provided_token = ProvidedIdToken::new(
|
|
INVALID_BANK_TOKEN.to_string(),
|
|
AuthorizationType::BankCard,
|
|
None,
|
|
);
|
|
let everest_mock = ModulePublisher::default();
|
|
|
|
let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
|
|
assert!(result.is_ok());
|
|
let validation_result = result.unwrap();
|
|
assert_eq!(
|
|
validation_result.authorization_status,
|
|
AuthorizationStatus::Invalid
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
/// Test validate_token with successful transaction
|
|
fn payment_terminal__validate_token__success() {
|
|
let mut feig_mock = SyncFeig::default();
|
|
feig_mock
|
|
.expect_begin_transaction()
|
|
.times(1)
|
|
.with(eq("valid_token"), eq(11))
|
|
.returning(|_, _| Ok(()));
|
|
|
|
let pt_module: PaymentTerminalModule = feig_mock.into();
|
|
|
|
let provided_token =
|
|
ProvidedIdToken::new("valid_token".to_string(), AuthorizationType::BankCard, None);
|
|
let everest_mock = ModulePublisher::default();
|
|
|
|
let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
|
|
assert!(result.is_ok());
|
|
let validation_result = result.unwrap();
|
|
assert_eq!(
|
|
validation_result.authorization_status,
|
|
AuthorizationStatus::Accepted
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
/// Test validate_token with transaction failures
|
|
fn payment_terminal__validate_token__transaction_failures() {
|
|
let parameters = [
|
|
(
|
|
ErrorMessages::CreditNotSufficient,
|
|
AuthorizationStatus::NoCredit,
|
|
),
|
|
(
|
|
ErrorMessages::PaymentMethodNotSupported,
|
|
AuthorizationStatus::Blocked,
|
|
),
|
|
(
|
|
ErrorMessages::AbortViaTimeoutOrAbortKey,
|
|
AuthorizationStatus::Invalid,
|
|
),
|
|
(
|
|
ErrorMessages::ReceiverNotReady,
|
|
AuthorizationStatus::Timeout,
|
|
),
|
|
(ErrorMessages::SystemError, AuthorizationStatus::Timeout),
|
|
(ErrorMessages::ErrorFromDialUp, AuthorizationStatus::Timeout),
|
|
(
|
|
ErrorMessages::PinProcessingNotPossible,
|
|
AuthorizationStatus::PinRequired,
|
|
),
|
|
(
|
|
ErrorMessages::NecessaryDeviceNotPresentOrDefective,
|
|
AuthorizationStatus::PinRequired,
|
|
),
|
|
];
|
|
|
|
for (error_code, expected_status) in parameters {
|
|
let mut feig_mock = SyncFeig::default();
|
|
feig_mock
|
|
.expect_begin_transaction()
|
|
.times(1)
|
|
.with(eq("valid_token"), eq(11))
|
|
.returning(move |_, _| Err(anyhow::Error::new(error_code)));
|
|
|
|
let pt_module: PaymentTerminalModule = feig_mock.into();
|
|
|
|
let provided_token =
|
|
ProvidedIdToken::new("valid_token".to_string(), AuthorizationType::BankCard, None);
|
|
let everest_mock = ModulePublisher::default();
|
|
|
|
let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
|
|
assert!(result.is_ok());
|
|
let validation_result = result.unwrap();
|
|
assert_eq!(
|
|
validation_result.authorization_status, expected_status,
|
|
"Failed for error code {:?}",
|
|
error_code
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Test validate_token with unknown error (no error code provided)
|
|
fn payment_terminal__validate_token__unknown_error() {
|
|
let mut feig_mock = SyncFeig::default();
|
|
feig_mock
|
|
.expect_begin_transaction()
|
|
.times(1)
|
|
.with(eq("valid_token"), eq(11))
|
|
.returning(|_, _| Err(anyhow::anyhow!("Some unknown error")));
|
|
|
|
let pt_module: PaymentTerminalModule = feig_mock.into();
|
|
|
|
let provided_token =
|
|
ProvidedIdToken::new("valid_token".to_string(), AuthorizationType::BankCard, None);
|
|
let everest_mock = ModulePublisher::default();
|
|
|
|
let result = pt_module.validate_token(&(&everest_mock).into(), provided_token);
|
|
assert!(result.is_ok());
|
|
let validation_result = result.unwrap();
|
|
assert_eq!(
|
|
validation_result.authorization_status,
|
|
AuthorizationStatus::Invalid
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
/// Test enable_card_reading with various scenarios
|
|
fn payment_terminal__enable_card_reading() {
|
|
type CardTypeMap = HashMap<AuthorizationType, Option<Vec<i64>>>;
|
|
|
|
let parameters: Vec<(CardTypeMap, i64, Vec<AuthorizationType>, CardTypeMap)> = vec![
|
|
// Init empty map - all card types supported
|
|
(
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![])),
|
|
(AuthorizationType::RFID, Some(vec![])),
|
|
]),
|
|
1,
|
|
vec![AuthorizationType::BankCard, AuthorizationType::RFID],
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![1])),
|
|
(AuthorizationType::RFID, Some(vec![1])),
|
|
]),
|
|
),
|
|
// Init empty map - with restrictions.
|
|
(
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![])),
|
|
(AuthorizationType::RFID, Some(vec![])),
|
|
]),
|
|
1,
|
|
vec![AuthorizationType::RFID],
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![])),
|
|
(AuthorizationType::RFID, Some(vec![1])),
|
|
]),
|
|
),
|
|
// Extend an existing map
|
|
(
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![3])),
|
|
(AuthorizationType::RFID, Some(vec![1])),
|
|
]),
|
|
2,
|
|
vec![AuthorizationType::BankCard, AuthorizationType::RFID],
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![2, 3])),
|
|
(AuthorizationType::RFID, Some(vec![1, 2])),
|
|
]),
|
|
),
|
|
// Extend an existing map
|
|
(
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![])),
|
|
(AuthorizationType::RFID, Some(vec![1])),
|
|
]),
|
|
2,
|
|
vec![AuthorizationType::BankCard, AuthorizationType::RFID],
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![2])),
|
|
(AuthorizationType::RFID, Some(vec![1, 2])),
|
|
]),
|
|
),
|
|
// Extend an existing map - idempotent (adding same connector twice)
|
|
(
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![1])),
|
|
(AuthorizationType::RFID, Some(vec![])),
|
|
]),
|
|
1,
|
|
vec![AuthorizationType::BankCard],
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![1])),
|
|
(AuthorizationType::RFID, Some(vec![])),
|
|
]),
|
|
),
|
|
// Extend an existing map - maintain sorted order when adding new
|
|
// connector
|
|
(
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![1, 3, 5])),
|
|
(AuthorizationType::RFID, Some(vec![])),
|
|
]),
|
|
2,
|
|
vec![AuthorizationType::BankCard],
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![1, 2, 3, 5])),
|
|
(AuthorizationType::RFID, Some(vec![])),
|
|
]),
|
|
),
|
|
// Removing
|
|
(
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![1, 2])),
|
|
(AuthorizationType::RFID, Some(vec![1, 2])),
|
|
]),
|
|
1,
|
|
vec![AuthorizationType::BankCard],
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![1, 2])),
|
|
(AuthorizationType::RFID, Some(vec![2])),
|
|
]),
|
|
),
|
|
// Removing - maintain sorted order when removing a connector
|
|
(
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![1, 2, 3, 5])),
|
|
(AuthorizationType::RFID, Some(vec![1, 2, 4])),
|
|
]),
|
|
2,
|
|
vec![],
|
|
HashMap::from_iter([
|
|
(AuthorizationType::BankCard, Some(vec![1, 3, 5])),
|
|
(AuthorizationType::RFID, Some(vec![1, 4])),
|
|
]),
|
|
),
|
|
];
|
|
|
|
for (initial_map, connector_id, supported_cards, expected_map) in parameters {
|
|
let feig_mock = SyncFeig::default();
|
|
let (tx, _) = channel();
|
|
let pt_module = PaymentTerminalModule {
|
|
tx,
|
|
feig: feig_mock,
|
|
card_type_to_connector: Mutex::new(initial_map),
|
|
pre_authorization_amount: Mutex::new(50),
|
|
};
|
|
|
|
let everest_mock = ModulePublisher::default();
|
|
|
|
let result = pt_module.enable_card_reading(
|
|
&(&everest_mock).into(),
|
|
connector_id,
|
|
supported_cards,
|
|
);
|
|
assert!(result.is_ok());
|
|
|
|
let actual_map = pt_module.card_type_to_connector.lock().unwrap();
|
|
assert_eq!(
|
|
*actual_map, expected_map,
|
|
"Failed for connector_id {}: expected {:?}, got {:?}",
|
|
connector_id, expected_map, *actual_map
|
|
);
|
|
}
|
|
}
|
|
}
|