Skip to content

Commit

Permalink
Allow to actually purchase an article
Browse files Browse the repository at this point in the history
  • Loading branch information
zargony committed Oct 29, 2024
1 parent 5f0eb33 commit 8ae9f36
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 12 deletions.
1 change: 0 additions & 1 deletion firmware/src/article.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ impl<const N: usize> Articles<N> {
}

/// Look up id of article at given index
#[allow(dead_code)]
pub fn id(&self, index: usize) -> Option<&ArticleId> {
self.ids.get(index)
}
Expand Down
4 changes: 3 additions & 1 deletion firmware/src/screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ impl Screen for PleaseWait {
FontColor::Transparent(BinaryColor::On),
target,
)?;
Footer::new("* Abbruch", "").draw(target)?;
if let Self::WifiConnecting = self {
Footer::new("* Abbruch", "").draw(target)?;
}
Ok(())
}
}
Expand Down
1 change: 0 additions & 1 deletion firmware/src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ pub fn uptime() -> Option<TimeDelta> {
}

/// Current time
#[allow(dead_code)]
pub fn now() -> Option<DateTime<Utc>> {
if let (Some(start_time), Some(uptime)) = (start_time(), uptime()) {
Some(start_time + uptime)
Expand Down
74 changes: 67 additions & 7 deletions firmware/src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::article::Articles;
use crate::article::{ArticleId, Articles};
use crate::buzzer::Buzzer;
use crate::display::Display;
use crate::error::Error;
Expand Down Expand Up @@ -182,21 +182,31 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
/// Run the user interface flow
pub async fn run(&mut self) -> Result<(), Error> {
// Wait for id card and verify identification
let userid = self.authenticate_user().await?;
let _user = self.users.get(userid);
let user_id = self.authenticate_user().await?;
let _user = self.users.get(user_id);

// Ask for number of drinks
let num_drinks = self.get_number_of_drinks().await?;

// Get article information
let article_id = self.articles.id(0).ok_or(Error::ArticleNotFound)?.clone();
let article = self.articles.get(0).ok_or(Error::ArticleNotFound)?;

// Calculate total price. It's ok to cast num_drinks to f32 as it's always a small number.
#[allow(clippy::cast_precision_loss)]
let total_price = article.price * num_drinks as f32;
let amount = num_drinks as f32;
let total_price = article.price * amount;

// Show total price and ask for confirmation
self.confirm_purchase(num_drinks, total_price).await?;

// TODO: Process payment
let _ = screen::Success::new(num_drinks);
let _ = self.show_error("Not implemented yet", true).await;
// Store purchase
self.purchase(&article_id, amount, user_id, total_price)
.await?;

// Show success and affirm to take drinks
self.show_success(num_drinks).await?;

Ok(())
}
}
Expand Down Expand Up @@ -293,4 +303,54 @@ impl<'a, I2C: I2c, IRQ: Wait<Error = Infallible>> Ui<'a, I2C, IRQ> {
}
}
}

/// Purchase the given article
async fn purchase(
&mut self,
article_id: &ArticleId,
amount: f32,
user_id: UserId,
total_price: f32,
) -> Result<(), Error> {
// Wait for network to become available (if not already)
self.wait_network_up().await?;

info!(
"UI: Purchasing {}x {}, {:.02} EUR for user {}...",
amount, article_id, total_price, user_id
);

self.display
.screen(&screen::PleaseWait::ApiQuerying)
.await?;
let mut vf = self.vereinsflieger.connect().await?;

// Store purchase
vf.purchase(article_id, amount, user_id, total_price)
.await?;

Ok(())
}

/// Show success screen and wait for keypress or timeout
async fn show_success(&mut self, num_drinks: usize) -> Result<(), Error> {
info!("UI: Displaying success, {} drinks", num_drinks);

self.display
.screen(&screen::Success::new(num_drinks))
.await?;
let _ = self.buzzer.confirm().await;

// Wait at least 1s without responding to keypad
let min_time = Duration::from_secs(1);
Timer::after(min_time).await;

let wait_cancel = async { while self.keypad.read().await != Key::Enter {} };
match with_timeout(USER_TIMEOUT - min_time, wait_cancel).await {
// Enter key continues
Ok(()) => Ok(()),
// User interaction timeout
Err(TimeoutError) => Err(Error::UserTimeout),
}
}
}
50 changes: 48 additions & 2 deletions firmware/src/vereinsflieger/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
mod proto_articles;
mod proto_auth;
mod proto_sale;
mod proto_user;

use crate::article::Articles;
use crate::article::{ArticleId, Articles};
use crate::http::{self, Http};
use crate::user::Users;
use crate::time;
use crate::user::{UserId, Users};
use crate::wifi::Wifi;
use alloc::format;
use alloc::string::String;
use core::cell::RefCell;
use core::fmt;
Expand Down Expand Up @@ -197,6 +200,40 @@ impl<'a> Connection<'a> {

Ok(())
}

/// Store a purchase
pub async fn purchase(
&mut self,
article_id: &ArticleId,
amount: f32,
user_id: UserId,
total_price: f32,
) -> Result<(), Error> {
use proto_sale::{SaleAddRequest, SaleAddResponse};

debug!(
"Vereinsflieger: Purchasing {}x {}, {:.02} EUR for user {}",
amount, article_id, total_price, user_id
);

let _response: SaleAddResponse = self
.connection
.post(
"sale/add",
&SaleAddRequest {
accesstoken: self.accesstoken,
bookingdate: &Self::today(),
articleid: article_id,
amount,
memberid: Some(user_id),
totalprice: Some(total_price),
comment: None,
},
)
.await?;
debug!("Vereinsflieger: Purchase successful");
Ok(())
}
}

impl<'a> Connection<'a> {
Expand Down Expand Up @@ -271,4 +308,13 @@ impl<'a> Connection<'a> {
None => Err(http::Error::Unauthorized.into()),
}
}

/// Helper function to get today's date as "yyyy-mm-dd" string
fn today() -> String {
if let Some(now) = time::now() {
format!("{}", now.format("%Y-%m-%d"))
} else {
String::new()
}
}
}
84 changes: 84 additions & 0 deletions firmware/src/vereinsflieger/proto_sale.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use super::AccessToken;
use crate::json::{self, FromJsonObject, ToJson};
use alloc::string::{String, ToString};
use embedded_io_async::{BufRead, Write};

/// `sale/add` request
#[derive(Debug)]
pub struct SaleAddRequest<'a> {
pub accesstoken: &'a AccessToken,
pub bookingdate: &'a str, // "yyyy-mm-dd"
pub articleid: &'a str,
pub amount: f32,
pub memberid: Option<u32>,
// pub callsign: Option<&'a str>,
// pub salestax: Option<f32>,
pub totalprice: Option<f32>,
// pub counter: Option<f32>,
pub comment: Option<&'a str>,
// pub ccid: Option<&'a str>,
// pub caid2: Option<u32>,
}

impl<'a> ToJson for SaleAddRequest<'a> {
async fn to_json<W: Write>(
&self,
json: &mut json::Writer<W>,
) -> Result<(), json::Error<W::Error>> {
let mut object = json.write_object().await?;
let mut object = object
.field("accesstoken", self.accesstoken)
.await?
.field("bookingdate", self.bookingdate)
.await?
.field("articleid", self.articleid)
.await?
.field("amount", self.amount)
.await?;
if let Some(memberid) = self.memberid {
object = object.field("memberid", memberid.to_string()).await?;
}
if let Some(totalprice) = self.totalprice {
object = object.field("totalprice", totalprice.to_string()).await?;
}
if let Some(comment) = self.comment {
object = object.field("comment", comment).await?;
}
object.finish().await
}
}

/// `sale/add` response
#[derive(Debug, Default)]
pub struct SaleAddResponse {
// pub createtime: String, // "yyyy-mm-dd hh:mm:ss"
// pub modifytime: String, // "yyyy-mm-dd hh:mm:ss"
// pub bookingdate: String, // "yyyy-mm-dd"
// pub callsign: String,
// pub comment: String,
// pub username: String,
// pub uid: u32,
// pub memberid: u32,
// pub amount: f32,
// pub netvalue: f32,
// pub salestax: f32,
// pub totalprice: f32,
// pub supid: u32,
// pub articleid: String,
// pub caid2: u32,
// pub httpstatuscode: u16,
}

impl FromJsonObject for SaleAddResponse {
type Context<'ctx> = ();

async fn read_next<R: BufRead>(
&mut self,
_key: String,
json: &mut json::Reader<R>,
_context: &Self::Context<'_>,
) -> Result<(), json::Error<R::Error>> {
_ = json.read_any().await?;
Ok(())
}
}

0 comments on commit 8ae9f36

Please sign in to comment.