refactor: Update listing confirmation flow and summary display
- Consolidate Create and Save button handling in confirmation flow - Add unsaved changes indicator to listing summary - Improve edit flow by using enter_edit_listing_draft function - Update confirmation button logic and user feedback messages - Clean up dialogue state management in confirmation handlers
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
case,
|
||||
commands::new_listing::{enter_edit_listing_draft, ListingDraft},
|
||||
db::{listing::PersistedListing, user::PersistedUser, ListingDAO, ListingDbId, UserDAO},
|
||||
keyboard_buttons,
|
||||
message_utils::{extract_callback_data, pluralize_with_count, send_message, MessageTarget},
|
||||
@@ -18,7 +19,6 @@ use teloxide::{
|
||||
pub enum MyListingsState {
|
||||
ViewingListings,
|
||||
ManagingListing(ListingDbId),
|
||||
EditingListing(ListingDbId),
|
||||
}
|
||||
impl From<MyListingsState> for DialogueRootState {
|
||||
fn from(state: MyListingsState) -> Self {
|
||||
@@ -122,21 +122,13 @@ async fn show_listings_for_user(
|
||||
)]);
|
||||
}
|
||||
|
||||
let mut response = format!(
|
||||
let response = format!(
|
||||
"📋 <b>My Listings</b>\n\n\
|
||||
You have {}.\n\n",
|
||||
You have {}.\n\n\
|
||||
Select a listing to view details",
|
||||
pluralize_with_count(listings.len(), "listing", "listings")
|
||||
);
|
||||
|
||||
// Add each listing with its ID and title
|
||||
for listing in &listings {
|
||||
response.push_str(&format!(
|
||||
"• <b>ID {}:</b> {}\n",
|
||||
listing.persisted.id, listing.base.title
|
||||
));
|
||||
}
|
||||
|
||||
response.push_str("\nTap a listing ID below to view details:");
|
||||
send_message(&bot, target, response, Some(keyboard)).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -169,15 +161,13 @@ async fn show_listing_details(
|
||||
let response = format!(
|
||||
"🔍 <b>Viewing Listing Details</b>\n\n\
|
||||
<b>Title:</b> {}\n\
|
||||
<b>Description:</b> {}\n\
|
||||
<b>ID:</b> {}",
|
||||
<b>Description:</b> {}\n",
|
||||
listing.base.title,
|
||||
listing
|
||||
.base
|
||||
.description
|
||||
.as_deref()
|
||||
.unwrap_or("No description"),
|
||||
listing.persisted.id
|
||||
);
|
||||
|
||||
send_message(
|
||||
@@ -207,9 +197,8 @@ async fn handle_managing_listing_callback(
|
||||
ManageListingButtons::Edit => {
|
||||
let (_, listing) =
|
||||
get_user_and_listing(&db_pool, &bot, from.id, listing_id, target.clone()).await?;
|
||||
dialogue
|
||||
.update(MyListingsState::EditingListing(listing.persisted.id))
|
||||
.await?;
|
||||
let draft = ListingDraft::from_persisted(listing);
|
||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||
}
|
||||
ManageListingButtons::Delete => {
|
||||
ListingDAO::delete_listing(&db_pool, listing_id).await?;
|
||||
|
||||
@@ -20,9 +20,11 @@ keyboard_buttons! {
|
||||
|
||||
keyboard_buttons! {
|
||||
pub enum ConfirmationKeyboardButtons {
|
||||
Save("✅ Save", "confirm_save"),
|
||||
Create("✅ Create", "confirm_create"),
|
||||
Edit("✏️ Edit", "confirm_edit"),
|
||||
Discard("🗑️ Discard", "confirm_discard"),
|
||||
Cancel("❌ Cancel", "confirm_cancel"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ async fn handle_new_listing_command(
|
||||
dialogue
|
||||
.update(NewListingState::AwaitingDraftField {
|
||||
field: ListingField::Title,
|
||||
draft: ListingDraft::draft_for_seller(user.persisted.id),
|
||||
draft: ListingDraft::new_for_seller(user.persisted.id),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -75,7 +75,7 @@ async fn handle_new_listing_command(
|
||||
}
|
||||
|
||||
async fn handle_awaiting_draft_field_input(
|
||||
bot: &Bot,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
(field, draft): (ListingField, ListingDraft),
|
||||
msg: Message,
|
||||
@@ -90,18 +90,18 @@ async fn handle_awaiting_draft_field_input(
|
||||
);
|
||||
|
||||
if is_cancel(text) {
|
||||
return cancel_wizard(bot, dialogue, chat).await;
|
||||
return cancel_wizard(&bot, dialogue, chat).await;
|
||||
}
|
||||
|
||||
match field {
|
||||
ListingField::Title => handle_title_input(bot, chat, text, dialogue, draft).await,
|
||||
ListingField::Title => handle_title_input(&bot, chat, text, dialogue, draft).await,
|
||||
ListingField::Description => {
|
||||
handle_description_input(bot, chat, text, dialogue, draft).await
|
||||
handle_description_input(&bot, chat, text, dialogue, draft).await
|
||||
}
|
||||
ListingField::Price => handle_price_input(bot, chat, text, dialogue, draft).await,
|
||||
ListingField::Slots => handle_slots_input(bot, chat, text, dialogue, draft).await,
|
||||
ListingField::StartTime => handle_start_time_input(bot, chat, text, dialogue, draft).await,
|
||||
ListingField::Duration => handle_duration_input(bot, chat, text, dialogue, draft).await,
|
||||
ListingField::Price => handle_price_input(&bot, chat, text, dialogue, draft).await,
|
||||
ListingField::Slots => handle_slots_input(&bot, chat, text, dialogue, draft).await,
|
||||
ListingField::StartTime => handle_start_time_input(&bot, chat, text, dialogue, draft).await,
|
||||
ListingField::Duration => handle_duration_input(&bot, chat, text, dialogue, draft).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ async fn handle_description_callback(
|
||||
}
|
||||
|
||||
async fn handle_awaiting_draft_field_callback(
|
||||
bot: &Bot,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
(field, draft): (ListingField, ListingDraft),
|
||||
callback_query: CallbackQuery,
|
||||
@@ -206,7 +206,7 @@ async fn handle_awaiting_draft_field_callback(
|
||||
let target = (from, message_id);
|
||||
|
||||
if data == "cancel" {
|
||||
return cancel_wizard(bot, dialogue, target).await;
|
||||
return cancel_wizard(&bot, dialogue, target).await;
|
||||
}
|
||||
|
||||
match field {
|
||||
@@ -216,7 +216,7 @@ async fn handle_awaiting_draft_field_callback(
|
||||
Ok(())
|
||||
}
|
||||
ListingField::Description => {
|
||||
handle_description_callback(bot, dialogue, draft, data.as_str(), target).await
|
||||
handle_description_callback(&bot, dialogue, draft, data.as_str(), target).await
|
||||
}
|
||||
ListingField::Price => {
|
||||
error!("Unknown callback data: {data}");
|
||||
@@ -224,13 +224,13 @@ async fn handle_awaiting_draft_field_callback(
|
||||
Ok(())
|
||||
}
|
||||
ListingField::Slots => {
|
||||
handle_slots_callback(bot, dialogue, draft, data.as_str(), target).await
|
||||
handle_slots_callback(&bot, dialogue, draft, data.as_str(), target).await
|
||||
}
|
||||
ListingField::StartTime => {
|
||||
handle_start_time_callback(bot, dialogue, draft, data.as_str(), target).await
|
||||
handle_start_time_callback(&bot, dialogue, draft, data.as_str(), target).await
|
||||
}
|
||||
ListingField::Duration => {
|
||||
handle_duration_callback(bot, dialogue, draft, data.as_str(), target).await
|
||||
handle_duration_callback(&bot, dialogue, draft, data.as_str(), target).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,39 +331,44 @@ async fn handle_viewing_draft_callback(
|
||||
callback_query: CallbackQuery,
|
||||
) -> HandlerResult {
|
||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||
let target = (from, message_id);
|
||||
|
||||
// Ensure the user exists before saving the listing
|
||||
UserDAO::find_or_create_by_telegram_user(&db_pool, from.clone())
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!("Error finding or creating user: {e}");
|
||||
})?;
|
||||
|
||||
let target = (from.clone(), message_id);
|
||||
|
||||
let button = ConfirmationKeyboardButtons::try_from(data.as_str())
|
||||
.map_err(|_| anyhow::anyhow!("Unknown ConfirmationKeyboardButtons data: {}", data))?;
|
||||
|
||||
match button {
|
||||
ConfirmationKeyboardButtons::Create => {
|
||||
ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => {
|
||||
info!("User {target:?} confirmed listing creation");
|
||||
save_listing(db_pool, bot, target, draft).await?;
|
||||
dialogue.exit().await?;
|
||||
}
|
||||
ConfirmationKeyboardButtons::Cancel => {
|
||||
info!("User {target:?} cancelled listing update");
|
||||
let response = "🗑️ <b>Changes Discarded</b>\n\n\
|
||||
Your changes have been discarded and not saved.";
|
||||
send_message(&bot, target, &response, None).await?;
|
||||
dialogue.exit().await?;
|
||||
save_listing(db_pool, bot, dialogue, target, draft).await?;
|
||||
}
|
||||
ConfirmationKeyboardButtons::Discard => {
|
||||
info!("User {target:?} discarded listing creation");
|
||||
|
||||
// Exit dialogue and send cancellation message
|
||||
dialogue.exit().await?;
|
||||
|
||||
let response = "🗑️ <b>Listing Discarded</b>\n\n\
|
||||
Your listing has been discarded and not created.\n\
|
||||
You can start a new listing anytime with /newlisting.";
|
||||
|
||||
send_message(&bot, target, &response, None).await?;
|
||||
dialogue.exit().await?;
|
||||
}
|
||||
ConfirmationKeyboardButtons::Edit => {
|
||||
info!("User {target:?} chose to edit listing");
|
||||
|
||||
// Delete the old message and show the edit screen
|
||||
show_edit_screen(&bot, target, &draft, None).await?;
|
||||
|
||||
// Go to editing state to allow user to modify specific fields
|
||||
dialogue
|
||||
.update(NewListingState::EditingDraft(draft))
|
||||
.await?;
|
||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,7 +588,12 @@ async fn display_listing_summary(
|
||||
response_lines.push(flash_message.to_string());
|
||||
}
|
||||
|
||||
response_lines.push("📋 <i><b>Listing Summary</b></i>".to_string());
|
||||
let unsaved_changes = if draft.has_changes {
|
||||
"<i>Unsaved changes</i>"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
response_lines.push(format!("📋 <b>Listing Summary</b> {unsaved_changes}"));
|
||||
response_lines.push("".to_string());
|
||||
response_lines.push(format!("<b>Title:</b> {}", draft.base.title));
|
||||
response_lines.push(format!(
|
||||
@@ -608,8 +618,14 @@ async fn display_listing_summary(
|
||||
response_lines.push(format!("<b>Duration:</b> {}", fields.end_delay));
|
||||
}
|
||||
ListingDraftPersisted::Persisted(fields) => {
|
||||
response_lines.push(format!("<b>Starts on:</b> {}", fields.start_at));
|
||||
response_lines.push(format!("<b>Ends on:</b> {}", fields.end_at));
|
||||
response_lines.push(format!(
|
||||
"<b>Starts on:</b> {}",
|
||||
format_datetime(fields.start_at)
|
||||
));
|
||||
response_lines.push(format!(
|
||||
"<b>Ends on:</b> {}",
|
||||
format_datetime(fields.end_at)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,20 +637,25 @@ async fn display_listing_summary(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_edit_screen(
|
||||
pub async fn enter_edit_listing_draft(
|
||||
bot: &Bot,
|
||||
target: impl Into<MessageTarget>,
|
||||
draft: &ListingDraft,
|
||||
draft: ListingDraft,
|
||||
dialogue: RootDialogue,
|
||||
flash_message: Option<&str>,
|
||||
) -> HandlerResult {
|
||||
display_listing_summary(
|
||||
bot,
|
||||
target,
|
||||
draft,
|
||||
&draft,
|
||||
Some(FieldSelectionKeyboardButtons::to_keyboard()),
|
||||
flash_message,
|
||||
)
|
||||
.await?;
|
||||
dialogue
|
||||
.update(NewListingState::EditingDraft(draft))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -643,19 +664,25 @@ async fn show_confirmation_screen(
|
||||
target: impl Into<MessageTarget>,
|
||||
draft: &ListingDraft,
|
||||
) -> HandlerResult {
|
||||
display_listing_summary(
|
||||
bot,
|
||||
target,
|
||||
draft,
|
||||
Some(ConfirmationKeyboardButtons::to_keyboard()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let keyboard = match draft.persisted {
|
||||
ListingDraftPersisted::New(_) => InlineKeyboardMarkup::default().append_row([
|
||||
ConfirmationKeyboardButtons::Create.to_button(),
|
||||
ConfirmationKeyboardButtons::Edit.to_button(),
|
||||
ConfirmationKeyboardButtons::Discard.to_button(),
|
||||
]),
|
||||
ListingDraftPersisted::Persisted(_) => InlineKeyboardMarkup::default().append_row([
|
||||
ConfirmationKeyboardButtons::Save.to_button(),
|
||||
ConfirmationKeyboardButtons::Edit.to_button(),
|
||||
ConfirmationKeyboardButtons::Cancel.to_button(),
|
||||
]),
|
||||
};
|
||||
|
||||
display_listing_summary(bot, target, draft, Some(keyboard), None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_editing_field_input(
|
||||
bot: &Bot,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
(field, draft): (ListingField, ListingDraft),
|
||||
msg: Message,
|
||||
@@ -667,22 +694,22 @@ async fn handle_editing_field_input(
|
||||
|
||||
match field {
|
||||
ListingField::Title => {
|
||||
handle_edit_title(bot, dialogue, draft, text, chat).await?;
|
||||
handle_edit_title(&bot, dialogue, draft, text, chat).await?;
|
||||
}
|
||||
ListingField::Description => {
|
||||
handle_edit_description(bot, dialogue, draft, text, chat).await?;
|
||||
handle_edit_description(&bot, dialogue, draft, text, chat).await?;
|
||||
}
|
||||
ListingField::Price => {
|
||||
handle_edit_price(bot, dialogue, draft, text, chat).await?;
|
||||
handle_edit_price(&bot, dialogue, draft, text, chat).await?;
|
||||
}
|
||||
ListingField::Slots => {
|
||||
handle_edit_slots(bot, dialogue, draft, text, chat).await?;
|
||||
handle_edit_slots(&bot, dialogue, draft, text, chat).await?;
|
||||
}
|
||||
ListingField::StartTime => {
|
||||
handle_edit_start_time(bot, dialogue, draft, text, chat).await?;
|
||||
handle_edit_start_time(&bot, dialogue, draft, text, chat).await?;
|
||||
}
|
||||
ListingField::Duration => {
|
||||
handle_edit_duration(bot, dialogue, draft, text, chat).await?;
|
||||
handle_edit_duration(&bot, dialogue, draft, text, chat).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,7 +717,7 @@ async fn handle_editing_field_input(
|
||||
}
|
||||
|
||||
async fn handle_editing_draft_callback(
|
||||
bot: &Bot,
|
||||
bot: Bot,
|
||||
draft: ListingDraft,
|
||||
dialogue: RootDialogue,
|
||||
callback_query: CallbackQuery,
|
||||
@@ -759,7 +786,7 @@ async fn handle_editing_draft_callback(
|
||||
create_back_button_keyboard_with(DurationKeyboardButtons::to_keyboard()),
|
||||
),
|
||||
FieldSelectionKeyboardButtons::Done => {
|
||||
show_confirmation_screen(bot, target, &draft).await?;
|
||||
show_confirmation_screen(&bot, target, &draft).await?;
|
||||
dialogue
|
||||
.update(DialogueRootState::NewListing(
|
||||
NewListingState::ViewingDraft(draft),
|
||||
@@ -790,13 +817,12 @@ async fn handle_editing_draft_callback(
|
||||
async fn save_listing(
|
||||
db_pool: SqlitePool,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
target: impl Into<MessageTarget>,
|
||||
draft: ListingDraft,
|
||||
) -> HandlerResult {
|
||||
let listing: PersistedListing = match draft.persisted {
|
||||
let (listing, success_message) = match draft.persisted {
|
||||
ListingDraftPersisted::New(fields) => {
|
||||
ListingDAO::insert_listing(
|
||||
let listing = ListingDAO::insert_listing(
|
||||
&db_pool,
|
||||
NewListing {
|
||||
persisted: fields,
|
||||
@@ -804,10 +830,11 @@ async fn save_listing(
|
||||
fields: draft.fields,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.await?;
|
||||
(listing, "Listing created!")
|
||||
}
|
||||
ListingDraftPersisted::Persisted(fields) => {
|
||||
ListingDAO::update_listing(
|
||||
let listing = ListingDAO::update_listing(
|
||||
&db_pool,
|
||||
PersistedListing {
|
||||
persisted: fields,
|
||||
@@ -815,21 +842,13 @@ async fn save_listing(
|
||||
fields: draft.fields,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.await?;
|
||||
(listing, "Listing updated!")
|
||||
}
|
||||
};
|
||||
|
||||
let response = format!(
|
||||
"✅ <b>Listing Created Successfully!</b>\n\n\
|
||||
<b>Listing ID:</b> {}\n\
|
||||
<b>Title:</b> {}\n\
|
||||
Your fixed price listing is now live! 🎉",
|
||||
listing.persisted.id, listing.base.title
|
||||
);
|
||||
|
||||
dialogue.exit().await?;
|
||||
let response = format!("✅ <b>{}</b>: {}", success_message, listing.base.title);
|
||||
send_message(&bot, target, response, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -857,14 +876,8 @@ async fn handle_edit_title(
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Go back to editing listing state
|
||||
show_edit_screen(bot, target, &draft, Some("✅ Title updated!")).await?;
|
||||
dialogue
|
||||
.update(DialogueRootState::NewListing(
|
||||
NewListingState::EditingDraft(draft),
|
||||
))
|
||||
.await?;
|
||||
draft.has_changes = true;
|
||||
enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Title updated!")).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -885,14 +898,16 @@ async fn handle_edit_description(
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
draft.has_changes = true;
|
||||
|
||||
// Go back to editing listing state
|
||||
show_edit_screen(bot, target, &draft, Some("✅ Description updated!")).await?;
|
||||
dialogue
|
||||
.update(DialogueRootState::NewListing(
|
||||
NewListingState::EditingDraft(draft),
|
||||
))
|
||||
.await?;
|
||||
enter_edit_listing_draft(
|
||||
bot,
|
||||
target,
|
||||
draft,
|
||||
dialogue,
|
||||
Some("✅ Description updated!"),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -919,15 +934,9 @@ async fn handle_edit_price(
|
||||
}
|
||||
};
|
||||
|
||||
// Go back to editing listing state
|
||||
show_edit_screen(bot, target, &draft, Some("✅ Price updated!")).await?;
|
||||
|
||||
dialogue
|
||||
.update(DialogueRootState::NewListing(
|
||||
NewListingState::EditingDraft(draft),
|
||||
))
|
||||
.await?;
|
||||
draft.has_changes = true;
|
||||
|
||||
enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Price updated!")).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -956,13 +965,9 @@ async fn handle_edit_slots(
|
||||
}
|
||||
};
|
||||
|
||||
show_edit_screen(bot, target, &draft, Some("✅ Slots updated!")).await?;
|
||||
dialogue
|
||||
.update(DialogueRootState::NewListing(
|
||||
NewListingState::EditingDraft(draft),
|
||||
))
|
||||
.await?;
|
||||
draft.has_changes = true;
|
||||
|
||||
enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Slots updated!")).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -989,13 +994,9 @@ async fn handle_edit_start_time(
|
||||
}
|
||||
};
|
||||
|
||||
// Go back to editing listing state
|
||||
show_edit_screen(bot, target, &draft, Some("✅ Start time updated!")).await?;
|
||||
dialogue
|
||||
.update(DialogueRootState::NewListing(
|
||||
NewListingState::EditingDraft(draft),
|
||||
))
|
||||
.await?;
|
||||
draft.has_changes = true;
|
||||
|
||||
enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Start time updated!")).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1021,18 +1022,14 @@ async fn handle_edit_duration(
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
draft.has_changes = true;
|
||||
|
||||
show_edit_screen(bot, target, &draft, Some("✅ Duration updated!")).await?;
|
||||
dialogue
|
||||
.update(DialogueRootState::NewListing(
|
||||
NewListingState::EditingDraft(draft),
|
||||
))
|
||||
.await?;
|
||||
enter_edit_listing_draft(bot, target, draft, dialogue, Some("✅ Duration updated!")).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_editing_draft_field_callback(
|
||||
bot: &Bot,
|
||||
bot: Bot,
|
||||
dialogue: RootDialogue,
|
||||
(field, draft): (ListingField, ListingDraft),
|
||||
callback_query: CallbackQuery,
|
||||
@@ -1041,33 +1038,28 @@ async fn handle_editing_draft_field_callback(
|
||||
let target = (from, message_id);
|
||||
info!("User {:?} editing field: {:?} -> {}", target, field, &data);
|
||||
if data == "edit_back" {
|
||||
show_edit_screen(bot, target, &draft, None).await?;
|
||||
dialogue
|
||||
.update(DialogueRootState::NewListing(
|
||||
NewListingState::EditingDraft(draft),
|
||||
))
|
||||
.await?;
|
||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match field {
|
||||
ListingField::Title => {
|
||||
handle_edit_title(bot, dialogue, draft, data.as_str(), target).await?;
|
||||
handle_edit_title(&bot, dialogue, draft, data.as_str(), target).await?;
|
||||
}
|
||||
ListingField::Description => {
|
||||
handle_edit_description(bot, dialogue, draft, data.as_str(), target).await?;
|
||||
handle_edit_description(&bot, dialogue, draft, data.as_str(), target).await?;
|
||||
}
|
||||
ListingField::Price => {
|
||||
handle_edit_price(bot, dialogue, draft, data.as_str(), target).await?;
|
||||
handle_edit_price(&bot, dialogue, draft, data.as_str(), target).await?;
|
||||
}
|
||||
ListingField::Slots => {
|
||||
handle_edit_slots(bot, dialogue, draft, data.as_str(), target).await?;
|
||||
handle_edit_slots(&bot, dialogue, draft, data.as_str(), target).await?;
|
||||
}
|
||||
ListingField::StartTime => {
|
||||
handle_edit_start_time(bot, dialogue, draft, data.as_str(), target).await?;
|
||||
handle_edit_start_time(&bot, dialogue, draft, data.as_str(), target).await?;
|
||||
}
|
||||
ListingField::Duration => {
|
||||
handle_edit_duration(bot, dialogue, draft, data.as_str(), target).await?;
|
||||
handle_edit_duration(&bot, dialogue, draft, data.as_str(), target).await?;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::{
|
||||
db::{
|
||||
listing::{ListingBase, ListingFields, NewListingFields, PersistedListingFields},
|
||||
listing::{
|
||||
ListingBase, ListingFields, NewListingFields, PersistedListing, PersistedListingFields,
|
||||
},
|
||||
MoneyAmount, UserDbId,
|
||||
},
|
||||
DialogueRootState,
|
||||
@@ -9,14 +11,16 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ListingDraft {
|
||||
pub has_changes: bool,
|
||||
pub persisted: ListingDraftPersisted,
|
||||
pub base: ListingBase,
|
||||
pub fields: ListingFields,
|
||||
}
|
||||
|
||||
impl ListingDraft {
|
||||
pub fn draft_for_seller(seller_id: UserDbId) -> Self {
|
||||
pub fn new_for_seller(seller_id: UserDbId) -> Self {
|
||||
Self {
|
||||
has_changes: false,
|
||||
persisted: ListingDraftPersisted::New(NewListingFields::default()),
|
||||
base: ListingBase {
|
||||
seller_id,
|
||||
@@ -29,6 +33,15 @@ impl ListingDraft {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_persisted(listing: PersistedListing) -> Self {
|
||||
Self {
|
||||
has_changes: false,
|
||||
persisted: ListingDraftPersisted::Persisted(listing.persisted),
|
||||
base: listing.base,
|
||||
fields: listing.fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -89,27 +89,34 @@ impl UserDAO {
|
||||
pool: &SqlitePool,
|
||||
user: teloxide::types::User,
|
||||
) -> Result<PersistedUser> {
|
||||
let mut tx = pool.begin().await?;
|
||||
let telegram_id = TelegramUserDbId::from(user.id);
|
||||
let binds = BindFields::default()
|
||||
.push("telegram_id", &TelegramUserDbId::from(user.id))
|
||||
.push("username", &user.username)
|
||||
.push("first_name", &user.first_name)
|
||||
.push("last_name", &user.last_name);
|
||||
|
||||
let user = sqlx::query_as(
|
||||
let query_str = format!(
|
||||
r#"
|
||||
INSERT INTO users (telegram_id, username, first_name, last_name)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO users ({})
|
||||
VALUES ({})
|
||||
ON CONFLICT (telegram_id) DO UPDATE SET
|
||||
username = EXCLUDED.username,
|
||||
first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name
|
||||
RETURNING id, telegram_id, username, first_name, last_name, is_banned, created_at, updated_at
|
||||
RETURNING {}
|
||||
"#,
|
||||
)
|
||||
.bind(telegram_id)
|
||||
.bind(user.username)
|
||||
.bind(user.first_name)
|
||||
.bind(user.last_name)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
binds.bind_names().join(", "),
|
||||
binds.bind_placeholders().join(", "),
|
||||
USER_RETURN_FIELDS.join(", ")
|
||||
);
|
||||
|
||||
let row = binds
|
||||
.bind_to_query(sqlx::query(&query_str))
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
let user = FromRow::from_row(&row)?;
|
||||
log::info!("load user from db: {:?}", user);
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
@@ -366,4 +373,145 @@ mod tests {
|
||||
|
||||
assert!(not_found_by_telegram.is_none());
|
||||
}
|
||||
|
||||
mod upsert_tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(None, None, None)]
|
||||
#[case(Some("new_user"), None, None)]
|
||||
#[case(None, Some("New First"), None)]
|
||||
#[case(None, None, Some("New Last"))]
|
||||
#[case(Some(""), None, Some(""))]
|
||||
#[tokio::test]
|
||||
async fn test_upsert_updates_fields(
|
||||
#[case] username: Option<&str>,
|
||||
#[case] first_name: Option<&str>,
|
||||
#[case] last_name: Option<&str>,
|
||||
) {
|
||||
let pool = create_test_pool().await;
|
||||
let user_id = UserId(12345);
|
||||
|
||||
let initial = teloxide::types::User {
|
||||
id: user_id,
|
||||
is_bot: false,
|
||||
first_name: "First".into(),
|
||||
last_name: Some("Last".into()),
|
||||
username: Some("user".into()),
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
|
||||
let created = UserDAO::find_or_create_by_telegram_user(&pool, initial)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut updated = teloxide::types::User {
|
||||
id: user_id,
|
||||
is_bot: false,
|
||||
first_name: "First".into(),
|
||||
last_name: Some("Last".into()),
|
||||
username: Some("user".into()),
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
if let Some(u) = username {
|
||||
updated.username = if u.is_empty() { None } else { Some(u.into()) };
|
||||
}
|
||||
if let Some(f) = first_name {
|
||||
updated.first_name = f.into();
|
||||
}
|
||||
if let Some(l) = last_name {
|
||||
updated.last_name = if l.is_empty() { None } else { Some(l.into()) };
|
||||
}
|
||||
|
||||
let result = UserDAO::find_or_create_by_telegram_user(&pool, updated.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(created.persisted.id, result.persisted.id);
|
||||
assert_eq!(result.username, updated.username);
|
||||
assert_eq!(result.first_name, updated.first_name);
|
||||
assert_eq!(result.last_name, updated.last_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_users_separate() {
|
||||
let pool = create_test_pool().await;
|
||||
|
||||
let user1 = teloxide::types::User {
|
||||
id: UserId(111),
|
||||
is_bot: false,
|
||||
first_name: "One".into(),
|
||||
last_name: None,
|
||||
username: Some("one".into()),
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
let user2 = teloxide::types::User {
|
||||
id: UserId(222),
|
||||
is_bot: false,
|
||||
first_name: "Two".into(),
|
||||
last_name: Some("Last".into()),
|
||||
username: None,
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
|
||||
let p1 = UserDAO::find_or_create_by_telegram_user(&pool, user1)
|
||||
.await
|
||||
.unwrap();
|
||||
let p2 = UserDAO::find_or_create_by_telegram_user(&pool, user2)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_ne!(p1.persisted.id, p2.persisted.id);
|
||||
assert_eq!(p1.telegram_id, UserId(111).into());
|
||||
assert_eq!(p2.telegram_id, UserId(222).into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_upsert_preserves_id_and_timestamps() {
|
||||
let pool = create_test_pool().await;
|
||||
|
||||
let user = teloxide::types::User {
|
||||
id: UserId(333),
|
||||
is_bot: false,
|
||||
first_name: "Original".into(),
|
||||
last_name: None,
|
||||
username: Some("orig".into()),
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
|
||||
let created = UserDAO::find_or_create_by_telegram_user(&pool, user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated_user = teloxide::types::User {
|
||||
id: UserId(333),
|
||||
is_bot: false,
|
||||
first_name: "Original".into(),
|
||||
last_name: None,
|
||||
username: Some("updated".into()),
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
|
||||
let updated = UserDAO::find_or_create_by_telegram_user(&pool, updated_user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(created.persisted.id, updated.persisted.id);
|
||||
assert_eq!(created.persisted.created_at, updated.persisted.created_at);
|
||||
assert_eq!(updated.username, Some("updated".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,6 @@ pub enum ListingFields {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::ListingDbId;
|
||||
use crate::db::{ListingDAO, TelegramUserDbId};
|
||||
use rstest::rstest;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct User<P: Debug + Clone> {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(unused)]
|
||||
pub struct PersistedUserFields {
|
||||
pub id: UserDbId,
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
@@ -34,6 +34,13 @@ macro_rules! keyboard_buttons {
|
||||
)*
|
||||
markup
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn to_button(self) -> teloxide::types::InlineKeyboardButton {
|
||||
match self {
|
||||
$($($name::$variant => teloxide::types::InlineKeyboardButton::callback($text, $callback_data)),*),*
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<$name> for teloxide::types::InlineKeyboardButton {
|
||||
fn from(value: $name) -> Self {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::HandlerResult;
|
||||
use anyhow::bail;
|
||||
use chrono::{DateTime, Utc};
|
||||
use num::One;
|
||||
use std::fmt::Display;
|
||||
use teloxide::{
|
||||
@@ -209,3 +210,7 @@ pub fn pluralize_with_count<N: One + PartialEq<N> + Display + Copy>(
|
||||
) -> String {
|
||||
format!("{} {}", count, pluralize(count, singular, plural))
|
||||
}
|
||||
|
||||
pub fn format_datetime(dt: DateTime<Utc>) -> String {
|
||||
dt.format("%b %d, %Y %H:%M UTC").to_string()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user