diff --git a/assets/i18n/en/main.ftl b/assets/i18n/en/main.ftl
index 03d1e73..bb8c7cd 100644
--- a/assets/i18n/en/main.ftl
+++ b/assets/i18n/en/main.ftl
@@ -222,10 +222,17 @@ checkout-contact = Contact details
checkout-shipping = Shipping address
checkout-email = Email
checkout-name = Full name
+checkout-phone = Phone
checkout-address = Address
checkout-city = City
checkout-zip = Postal code
checkout-country = Country
+country-sk = Slovakia
+country-cz = Czechia
+country-at = Austria
+country-de = Germany
+country-pl = Poland
+country-hu = Hungary
checkout-note = Order note
checkout-place-order = Place order
checkout-summary = Order summary
diff --git a/assets/i18n/sk/main.ftl b/assets/i18n/sk/main.ftl
index bd4f87b..0f6d666 100644
--- a/assets/i18n/sk/main.ftl
+++ b/assets/i18n/sk/main.ftl
@@ -222,10 +222,17 @@ checkout-contact = Kontaktné údaje
checkout-shipping = Dodacia adresa
checkout-email = E-mail
checkout-name = Meno a priezvisko
+checkout-phone = Telefón
checkout-address = Adresa
checkout-city = Mesto
checkout-zip = PSČ
checkout-country = Krajina
+country-sk = Slovensko
+country-cz = Česko
+country-at = Rakúsko
+country-de = Nemecko
+country-pl = Poľsko
+country-hu = Maďarsko
checkout-note = Poznámka k objednávke
checkout-place-order = Odoslať objednávku
checkout-summary = Súhrn objednávky
diff --git a/assets/views/admin/orders/show.html b/assets/views/admin/orders/show.html
index e60327d..ba153b2 100644
--- a/assets/views/admin/orders/show.html
+++ b/assets/views/admin/orders/show.html
@@ -51,6 +51,7 @@
{{ t(key="order-customer", lang=lang | default(value='sk')) }}
{{ order.customer_name }}
{{ order.email }}
+ {% if order.phone %}{{ order.phone }}
{% endif %}
+
+
+
+
+
+
+
@@ -66,8 +85,15 @@
-
+
+
+
+
+
+
+
diff --git a/migration/src/lib.rs b/migration/src/lib.rs
index 797109f..d84281f 100644
--- a/migration/src/lib.rs
+++ b/migration/src/lib.rs
@@ -29,6 +29,7 @@ mod m20260616_150812_add_shipping_fields_to_orders;
mod m20260616_160000_add_parent_to_categories;
mod m20260617_000001_add_carrier_to_shipping_methods;
mod m20260617_000002_add_shipment_to_orders;
+mod m20260617_000003_add_phone_to_orders;
pub struct Migrator;
#[async_trait::async_trait]
@@ -62,6 +63,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260616_160000_add_parent_to_categories::Migration),
Box::new(m20260617_000001_add_carrier_to_shipping_methods::Migration),
Box::new(m20260617_000002_add_shipment_to_orders::Migration),
+ Box::new(m20260617_000003_add_phone_to_orders::Migration),
// inject-above (do not remove this comment)
]
}
diff --git a/migration/src/m20260617_000003_add_phone_to_orders.rs b/migration/src/m20260617_000003_add_phone_to_orders.rs
new file mode 100644
index 0000000..072699e
--- /dev/null
+++ b/migration/src/m20260617_000003_add_phone_to_orders.rs
@@ -0,0 +1,17 @@
+use loco_rs::schema::*;
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
+ // Customer contact phone, also passed to carriers for pickup SMS.
+ add_column(m, "orders", "phone", ColType::StringNull).await
+ }
+
+ async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
+ remove_column(m, "orders", "phone").await
+ }
+}
diff --git a/src/controllers/admin_orders.rs b/src/controllers/admin_orders.rs
index bddeb74..d60390f 100644
--- a/src/controllers/admin_orders.rs
+++ b/src/controllers/admin_orders.rs
@@ -178,6 +178,7 @@ async fn ship(
order_number: &order.order_number,
recipient_name: recipient,
email: &order.email,
+ phone: order.phone.as_deref(),
address: order.address.as_deref(),
city: order.city.as_deref(),
zip: order.zip.as_deref(),
diff --git a/src/controllers/checkout.rs b/src/controllers/checkout.rs
index eb72b53..9375273 100644
--- a/src/controllers/checkout.rs
+++ b/src/controllers/checkout.rs
@@ -21,6 +21,8 @@ const PAYMENT_METHODS: [&str; 2] = ["cod", "bank_transfer"];
#[derive(Debug, Deserialize)]
struct CheckoutForm {
email: String,
+ phone_prefix: String,
+ phone: String,
customer_name: String,
address: String,
city: String,
@@ -111,6 +113,25 @@ async fn place_order(
}
let email =
trimmed(&form.email).ok_or_else(|| Error::BadRequest("email is required".to_string()))?;
+ // Combine the dialling-code prefix with the local number into one E.164-ish
+ // value (e.g. "+421 900123456").
+ let number =
+ trimmed(&form.phone).ok_or_else(|| Error::BadRequest("phone is required".to_string()))?;
+ let phone = match trimmed(&form.phone_prefix) {
+ Some(prefix) => format!("{prefix} {number}"),
+ None => number,
+ };
+
+ // Contact and shipping-address fields are mandatory (also enforced in the
+ // browser via `required`).
+ let require = |value: &str, field: &str| -> Result {
+ trimmed(value).ok_or_else(|| Error::BadRequest(format!("{field} is required")))
+ };
+ let customer_name = require(&form.customer_name, "name")?;
+ let address = require(&form.address, "address")?;
+ let city = require(&form.city, "city")?;
+ let zip = require(&form.zip, "zip")?;
+ let country = require(&form.country, "country")?;
if !PAYMENT_METHODS.contains(&form.payment_method.as_str()) {
return Err(Error::BadRequest("invalid payment method".to_string()));
@@ -141,11 +162,12 @@ async fn place_order(
&valid,
orders::Checkout {
email,
- customer_name: trimmed(&form.customer_name),
- address: trimmed(&form.address),
- city: trimmed(&form.city),
- zip: trimmed(&form.zip),
- country: trimmed(&form.country),
+ phone,
+ customer_name: Some(customer_name),
+ address: Some(address),
+ city: Some(city),
+ zip: Some(zip),
+ country: Some(country),
note: form.note.as_deref().and_then(trimmed),
payment_method: form.payment_method,
method,
diff --git a/src/integrations/mod.rs b/src/integrations/mod.rs
index 4d20173..c0bdb36 100644
--- a/src/integrations/mod.rs
+++ b/src/integrations/mod.rs
@@ -19,6 +19,7 @@ pub struct ShipmentRequest<'a> {
pub order_number: &'a str,
pub recipient_name: &'a str,
pub email: &'a str,
+ pub phone: Option<&'a str>,
pub address: Option<&'a str>,
pub city: Option<&'a str>,
pub zip: Option<&'a str>,
diff --git a/src/integrations/packeta.rs b/src/integrations/packeta.rs
index 21ea1b6..ec495d1 100644
--- a/src/integrations/packeta.rs
+++ b/src/integrations/packeta.rs
@@ -60,6 +60,7 @@ pub async fn create_shipment(ctx: &AppContext, req: ShipmentRequest<'_>) -> Resu
{}\
-\
{}\
+ {}\
{}\
{:.2}\
{:.2}\
@@ -72,6 +73,7 @@ pub async fn create_shipment(ctx: &AppContext, req: ShipmentRequest<'_>) -> Resu
xml_escape(req.order_number),
xml_escape(req.recipient_name),
xml_escape(req.email),
+ xml_escape(req.phone.unwrap_or("")),
xml_escape(address_id),
value,
cod,
diff --git a/src/models/_entities/orders.rs b/src/models/_entities/orders.rs
index 2cabdd9..fe113d2 100644
--- a/src/models/_entities/orders.rs
+++ b/src/models/_entities/orders.rs
@@ -13,6 +13,7 @@ pub struct Model {
#[sea_orm(unique)]
pub order_number: String,
pub email: String,
+ pub phone: Option,
pub customer_name: Option,
pub status: String,
pub total_cents: i64,
diff --git a/src/models/orders.rs b/src/models/orders.rs
index fbfcf77..a49768a 100644
--- a/src/models/orders.rs
+++ b/src/models/orders.rs
@@ -12,6 +12,7 @@ pub type Orders = Entity;
/// database inside [`place`] so the customer cannot influence what they pay.
pub struct Checkout {
pub email: String,
+ pub phone: String,
pub customer_name: Option,
pub address: Option,
pub city: Option,
@@ -64,6 +65,7 @@ pub async fn place(ctx: &AppContext, items: &[(i32, i32)], details: Checkout) ->
let order = ActiveModel {
order_number: Set(generate_order_number()),
email: Set(details.email),
+ phone: Set(Some(details.phone)),
customer_name: Set(details.customer_name),
status: Set("pending".to_string()),
total_cents: Set(subtotal + details.method.price_cents),
diff --git a/src/views/checkout.rs b/src/views/checkout.rs
index b9a7750..015d7ee 100644
--- a/src/views/checkout.rs
+++ b/src/views/checkout.rs
@@ -28,6 +28,7 @@ pub fn detail(order: &orders::Model, bank_iban: &str, bank_account_name: &str) -
"id": order.id,
"order_number": order.order_number,
"email": order.email,
+ "phone": order.phone,
"customer_name": order.customer_name,
"status": order.status,
"subtotal": format_price(order.total_cents - order.shipping_cents),