add reward card feature with barcodes
This commit is contained in:
+75
-1
@@ -23,7 +23,9 @@ use tracing::{Level, error, warn};
|
||||
use crate::assets;
|
||||
use crate::domain::{DomainError, SessionUser};
|
||||
use crate::ports::{HubEvent, RealtimeNotifier};
|
||||
use crate::services::{AuthService, InvitationService, ListService, MealService};
|
||||
use crate::services::{
|
||||
AuthService, InvitationService, ListService, MealService, RewardsCardService,
|
||||
};
|
||||
use crate::views;
|
||||
use crate::webauthn::WebAuthnService;
|
||||
|
||||
@@ -33,6 +35,7 @@ pub struct AppState {
|
||||
pub lists: Arc<ListService>,
|
||||
pub meals: Arc<MealService>,
|
||||
pub invitations: Arc<InvitationService>,
|
||||
pub rewards_cards: Arc<RewardsCardService>,
|
||||
pub webauthn: Arc<WebAuthnService>,
|
||||
pub realtime: Arc<dyn RealtimeNotifier>,
|
||||
pub cookie_secure: bool,
|
||||
@@ -130,6 +133,8 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/lists/{list_id}/meals/{list_meal_id}/remove",
|
||||
post(remove_meal_from_list),
|
||||
)
|
||||
.route("/rewards", get(rewards_page).post(create_rewards_card))
|
||||
.route("/rewards/{card_id}/delete", post(delete_rewards_card))
|
||||
.route("/lists/{list_id}/stream", get(list_stream))
|
||||
.route("/invite/{token}", get(invitation_page))
|
||||
.route("/invite/{token}/accept", post(accept_invitation))
|
||||
@@ -261,6 +266,14 @@ struct CategoryForm {
|
||||
csrf: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RewardsCardForm {
|
||||
store_name: String,
|
||||
number: String,
|
||||
symbology: String,
|
||||
csrf: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PasskeyRegisterStartForm {
|
||||
csrf: String,
|
||||
@@ -819,6 +832,67 @@ async fn delete_meal_category(
|
||||
Ok(Redirect::to("/meals").into_response())
|
||||
}
|
||||
|
||||
async fn rewards_page(
|
||||
State(state): State<AppState>,
|
||||
user: CurrentUser,
|
||||
) -> Result<Response, AppError> {
|
||||
let cards = state.rewards_cards.list_cards(user.session.user.id).await?;
|
||||
Ok(html_response(views::rewards_page(
|
||||
&user.session.user,
|
||||
&cards,
|
||||
&user.session.csrf_token,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn create_rewards_card(
|
||||
State(state): State<AppState>,
|
||||
user: CurrentUser,
|
||||
LoggedForm(form): LoggedForm<RewardsCardForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
verify_csrf(&user, &form.csrf)?;
|
||||
let store_name = form.store_name.trim().to_owned();
|
||||
let number = form.number.trim().to_owned();
|
||||
let symbology = normalize_symbology(&form.symbology);
|
||||
if store_name.is_empty() || store_name.chars().count() > 60 {
|
||||
return Err(AppError::BadRequest(
|
||||
"Store names must be between 1 and 60 characters.".into(),
|
||||
));
|
||||
}
|
||||
if number.is_empty() || number.chars().count() > 80 {
|
||||
return Err(AppError::BadRequest(
|
||||
"Card numbers must be between 1 and 80 characters.".into(),
|
||||
));
|
||||
}
|
||||
state
|
||||
.rewards_cards
|
||||
.create_card(user.session.user.id, store_name, number, symbology)
|
||||
.await?;
|
||||
Ok(Redirect::to("/rewards").into_response())
|
||||
}
|
||||
|
||||
async fn delete_rewards_card(
|
||||
State(state): State<AppState>,
|
||||
user: CurrentUser,
|
||||
Path(card_id): Path<i64>,
|
||||
LoggedForm(form): LoggedForm<CsrfForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
verify_csrf(&user, &form.csrf)?;
|
||||
state
|
||||
.rewards_cards
|
||||
.delete_card(user.session.user.id, card_id)
|
||||
.await?;
|
||||
Ok(Redirect::to("/rewards").into_response())
|
||||
}
|
||||
|
||||
/// Normalizes a submitted symbology value to a known barcode type, falling
|
||||
/// back to Code 128 for unknown or empty values.
|
||||
fn normalize_symbology(value: &str) -> String {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"code39" => "code39".to_owned(),
|
||||
_ => "code128".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn meals_page(
|
||||
State(state): State<AppState>,
|
||||
user: CurrentUser,
|
||||
|
||||
Reference in New Issue
Block a user