From d1f9838890bc3724b94556454c32d2467a4ec325 Mon Sep 17 00:00:00 2001 From: Priec Date: Sat, 27 Jun 2026 14:27:37 +0200 Subject: [PATCH] admin panel have more control over payment now --- assets/i18n/en/main.ftl | 6 + assets/i18n/sk/main.ftl | 6 + assets/views/admin/base.html | 4 + assets/views/admin/payments/index.html | 47 ++++++++ assets/views/shop/checkout.html | 14 ++- migration/src/lib.rs | 2 + .../src/m20260627_000002_payment_settings.rs | 41 +++++++ src/app.rs | 4 +- src/controllers/account.rs | 5 +- src/controllers/admin_orders.rs | 5 +- src/controllers/admin_payments.rs | 112 ++++++++++++++++++ src/controllers/checkout.rs | 26 +++- src/controllers/mod.rs | 1 + src/initializers/mod.rs | 1 + src/initializers/payment_seeder.rs | 73 ++++++++++++ src/models/_entities/mod.rs | 2 + src/models/_entities/payment_methods.rs | 21 ++++ src/models/_entities/prelude.rs | 2 + src/models/_entities/shop_settings.rs | 20 ++++ src/models/mod.rs | 2 + src/models/payment_methods.rs | 54 +++++++++ src/models/shop_settings.rs | 47 ++++++++ src/shared/settings.rs | 19 +++ 23 files changed, 497 insertions(+), 17 deletions(-) create mode 100644 assets/views/admin/payments/index.html create mode 100644 migration/src/m20260627_000002_payment_settings.rs create mode 100644 src/controllers/admin_payments.rs create mode 100644 src/initializers/payment_seeder.rs create mode 100644 src/models/_entities/payment_methods.rs create mode 100644 src/models/_entities/shop_settings.rs create mode 100644 src/models/payment_methods.rs create mode 100644 src/models/shop_settings.rs diff --git a/assets/i18n/en/main.ftl b/assets/i18n/en/main.ftl index 5b237c1..9608403 100644 --- a/assets/i18n/en/main.ftl +++ b/assets/i18n/en/main.ftl @@ -368,6 +368,7 @@ country-pl = Poland country-hu = Hungary checkout-note = Order note checkout-save-profile = Save residence address to my profile +payment-none = No payment method is currently available. account-type = Account type account-personal = Individual account-company = Company @@ -483,6 +484,11 @@ bank-variable-symbol = Variable symbol bank-amount = Amount admin-shipping = Shipping admin-shipping-desc = set the price and availability of each delivery option. +admin-payments = Payments +admin-payments-desc = enable or disable payment methods and edit bank-transfer details. +payment-methods = Payment methods +payment-enabled = Active +payment-bank-settings = Bank transfer details shipping-enabled = Active admin-currency = Exchange rate admin-currency-desc = set the exchange rate for the currencies customers can switch between. You always enter prices in EUR. diff --git a/assets/i18n/sk/main.ftl b/assets/i18n/sk/main.ftl index 3af0294..d75b178 100644 --- a/assets/i18n/sk/main.ftl +++ b/assets/i18n/sk/main.ftl @@ -368,6 +368,7 @@ country-pl = Poľsko country-hu = Maďarsko checkout-note = Poznámka k objednávke checkout-save-profile = Uložiť adresu bydliska do môjho profilu +payment-none = Momentálne nie je dostupný žiadny spôsob platby. account-type = Typ účtu account-personal = Súkromná osoba account-company = Firma @@ -483,6 +484,11 @@ bank-variable-symbol = Variabilný symbol bank-amount = Suma admin-shipping = Doprava admin-shipping-desc = nastaviť cenu a dostupnosť jednotlivých možností dopravy. +admin-payments = Platby +admin-payments-desc = zapnite alebo vypnite spôsoby platby a upravte údaje pre prevod na účet. +payment-methods = Spôsoby platby +payment-enabled = Aktívne +payment-bank-settings = Údaje pre prevod na účet shipping-enabled = Aktívne admin-currency = Kurz admin-currency-desc = nastaviť výmenný kurz pre meny, medzi ktorými môžu zákazníci prepínať. Ceny zadávate vždy v EUR. diff --git a/assets/views/admin/base.html b/assets/views/admin/base.html index 895f86e..91dd1a8 100644 --- a/assets/views/admin/base.html +++ b/assets/views/admin/base.html @@ -105,6 +105,10 @@ class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong"> {{ t(key="admin-shipping", lang=lang | default(value='sk')) }} + + {{ t(key="admin-payments", lang=lang | default(value='sk')) }} + {{ t(key="admin-currency", lang=lang | default(value='sk')) }} diff --git a/assets/views/admin/payments/index.html b/assets/views/admin/payments/index.html new file mode 100644 index 0000000..48d6d0d --- /dev/null +++ b/assets/views/admin/payments/index.html @@ -0,0 +1,47 @@ +{% extends "admin/base.html" %} +{% import "macros/ui.html" as ui %} + +{% block title %}{{ t(key="admin-payments", lang=lang | default(value='sk')) }}{% endblock title %} +{% block crumb %}{{ t(key="admin-payments", lang=lang | default(value='sk')) }}{% endblock crumb %} + +{% block content %} +
+

{{ t(key="admin-payments", lang=lang | default(value='sk')) }}

+

{{ t(key="admin-payments-desc", lang=lang | default(value='sk')) }}

+
+ +
+

{{ t(key="payment-methods", lang=lang | default(value='sk')) }}

+ {% for method in methods %} +
+ {{ ui::csrf_field() }} +
+

{{ t(key=method.label_key, lang=lang | default(value='sk')) }}

+

{{ method.code }}

+
+
{{ ui::checkbox(name="enabled", label=t(key="payment-enabled", lang=lang | default(value='sk')), checked=method.enabled) }}
+ {{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit", extra="ml-auto") }} +
+ {% endfor %} +
+ +
+

{{ t(key="payment-bank-settings", lang=lang | default(value='sk')) }}

+
+ {{ ui::csrf_field() }} +
+ + {{ ui::input(name="bank_account_name", id="bank_account_name", value=bank_account_name) }} +
+
+ + {{ ui::input(name="bank_iban", id="bank_iban", value=bank_iban) }} +
+
+ {{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit") }} +
+
+
+{% endblock content %} diff --git a/assets/views/shop/checkout.html b/assets/views/shop/checkout.html index 5578651..1ab3e1b 100644 --- a/assets/views/shop/checkout.html +++ b/assets/views/shop/checkout.html @@ -266,14 +266,16 @@
{{ t(key="checkout-payment", lang=lang | default(value='sk')) }}{{ ui::req() }} + {% if payment_methods | length > 0 %} + {% for method in payment_methods %} - + {% endfor %} + {% else %} +

{{ t(key="payment-none", lang=lang | default(value='sk')) }}

+ {% endif %}
diff --git a/migration/src/lib.rs b/migration/src/lib.rs index d8eb988..0b63257 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -51,6 +51,7 @@ mod m20260623_000003_drop_currency; mod m20260623_000004_currencies; mod m20260625_000001_add_avatar_to_users; mod m20260627_000001_order_residence_address; +mod m20260627_000002_payment_settings; pub struct Migrator; #[async_trait::async_trait] @@ -106,6 +107,7 @@ impl MigratorTrait for Migrator { Box::new(m20260623_000004_currencies::Migration), Box::new(m20260625_000001_add_avatar_to_users::Migration), Box::new(m20260627_000001_order_residence_address::Migration), + Box::new(m20260627_000002_payment_settings::Migration), // inject-above (do not remove this comment) ] } diff --git a/migration/src/m20260627_000002_payment_settings.rs b/migration/src/m20260627_000002_payment_settings.rs new file mode 100644 index 0000000..11abc2a --- /dev/null +++ b/migration/src/m20260627_000002_payment_settings.rs @@ -0,0 +1,41 @@ +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> { + create_table( + m, + "payment_methods", + &[ + ("id", ColType::PkAuto), + ("code", ColType::StringUniq), + ("name", ColType::String), + ("enabled", ColType::BooleanWithDefault(true)), + ("position", ColType::IntegerWithDefault(0)), + ], + &[], + ) + .await?; + + create_table( + m, + "shop_settings", + &[ + ("id", ColType::PkAuto), + ("key", ColType::StringUniq), + ("value", ColType::TextNull), + ], + &[], + ) + .await + } + + async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> { + drop_table(m, "shop_settings").await?; + drop_table(m, "payment_methods").await + } +} diff --git a/src/app.rs b/src/app.rs index 07a0c1f..6d1364a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,7 +18,7 @@ use std::{path::Path, sync::Arc}; use crate::{ controllers::{ account, admin_categories, admin_currencies, admin_customers, admin_dashboard, - admin_discount_profiles, admin_form, admin_orders, admin_products, admin_shipping, + admin_discount_profiles, admin_form, admin_orders, admin_payments, admin_products, admin_shipping, auth, auth_pages, cart, checkout, currency, home, i18n, media, oauth2, pages, shop, }, @@ -83,6 +83,7 @@ impl Hooks for App { Box::new(initializers::view_engine::ViewEngineInitializer), Box::new(initializers::admin_seeder::AdminSeeder), Box::new(initializers::shipping_seeder::ShippingSeeder), + Box::new(initializers::payment_seeder::PaymentSeeder), Box::new(initializers::currency_seeder::CurrencySeeder), Box::new(initializers::oauth2::OAuth2StoreInitializer), Box::new(initializers::oauth2_session::OAuth2SessionInitializer), @@ -111,6 +112,7 @@ impl Hooks for App { .add_route(admin_discount_profiles::routes()) .add_route(admin_categories::routes()) .add_route(admin_orders::routes()) + .add_route(admin_payments::routes()) .add_route(admin_customers::routes()) .add_route(admin_shipping::routes()) .add_route(admin_currencies::routes()) diff --git a/src/controllers/account.rs b/src/controllers/account.rs index 4e72be5..e04e34b 100644 --- a/src/controllers/account.rs +++ b/src/controllers/account.rs @@ -335,6 +335,7 @@ async fn order_detail_page( .all(&ctx.db) .await?; + let (bank_iban, bank_account_name) = settings::bank_details(&ctx).await?; format::view( &v, "account/order_detail.html", @@ -347,8 +348,8 @@ async fn order_detail_page( "customer_avatar": user.avatar_id, "order": order_view::detail( &order, - settings::get(&ctx, "bank_iban").unwrap_or(""), - settings::get(&ctx, "bank_account_name").unwrap_or(""), + &bank_iban, + &bank_account_name, ), "items": order_view::items(&items), "lang": current_lang(&jar), diff --git a/src/controllers/admin_orders.rs b/src/controllers/admin_orders.rs index fa6d01d..9a9402a 100644 --- a/src/controllers/admin_orders.rs +++ b/src/controllers/admin_orders.rs @@ -93,6 +93,7 @@ async fn render_show( .await?; let carrier = order_carrier(ctx, &order).await?; + let (bank_iban, bank_account_name) = settings::bank_details(ctx).await?; // The order can be sent only if it maps to a real carrier and hasn't been // dispatched yet. let can_ship = carrier != "none" && order.tracking_number.is_none(); @@ -103,8 +104,8 @@ async fn render_show( json!({ "order": view::detail( &order, - settings::get(ctx, "bank_iban").unwrap_or(""), - settings::get(ctx, "bank_account_name").unwrap_or(""), + &bank_iban, + &bank_account_name, ), "items": view::items(&items), "statuses": ORDER_STATUSES, diff --git a/src/controllers/admin_payments.rs b/src/controllers/admin_payments.rs new file mode 100644 index 0000000..954cbb7 --- /dev/null +++ b/src/controllers/admin_payments.rs @@ -0,0 +1,112 @@ +//! Admin management for checkout payment methods and bank-transfer details. + +use axum_extra::extract::cookie::CookieJar; +use loco_rs::prelude::*; +use sea_orm::{ActiveModelTrait, EntityTrait, QueryOrder, Set}; +use serde::Deserialize; +use serde_json::json; + +use crate::{ + controllers::i18n::current_lang, + models::{payment_methods, shop_settings}, + shared::guard, +}; + +#[derive(Debug, Deserialize)] +struct PaymentMethodForm { + enabled: Option, +} + +#[derive(Debug, Deserialize)] +struct BankSettingsForm { + bank_account_name: String, + bank_iban: String, +} + +fn is_checked(value: &Option) -> bool { + matches!(value.as_deref(), Some("on" | "true" | "1")) +} + +fn trimmed(value: &str) -> Option { + let value = value.trim(); + (!value.is_empty()).then(|| value.to_string()) +} + +#[debug_handler] +async fn index( + auth: auth::JWT, + jar: CookieJar, + ViewEngine(v): ViewEngine, + State(ctx): State, +) -> Result { + guard::current_admin(auth, &ctx).await?; + let methods = payment_methods::Entity::find() + .order_by_asc(payment_methods::Column::Position) + .all(&ctx.db) + .await?; + let rows: Vec = methods + .iter() + .map(|m| { + json!({ + "id": m.id, + "code": m.code, + "label_key": m.label_key(), + "enabled": m.enabled, + }) + }) + .collect(); + let bank_account_name = shop_settings::Entity::get(&ctx.db, "bank_account_name") + .await? + .unwrap_or_default(); + let bank_iban = shop_settings::Entity::get(&ctx.db, "bank_iban") + .await? + .unwrap_or_default(); + + format::view( + &v, + "admin/payments/index.html", + json!({ + "methods": rows, + "bank_account_name": bank_account_name, + "bank_iban": bank_iban, + "lang": current_lang(&jar), + }), + ) +} + +#[debug_handler] +async fn update_method( + auth: auth::JWT, + Path(id): Path, + State(ctx): State, + Form(form): Form, +) -> Result { + guard::current_admin(auth, &ctx).await?; + let method = payment_methods::Entity::find_by_id(id) + .one(&ctx.db) + .await? + .ok_or_else(|| Error::NotFound)?; + let mut active = method.into_active_model(); + active.enabled = Set(is_checked(&form.enabled)); + active.update(&ctx.db).await?; + format::redirect("/admin/payments") +} + +#[debug_handler] +async fn update_bank( + auth: auth::JWT, + State(ctx): State, + Form(form): Form, +) -> Result { + guard::current_admin(auth, &ctx).await?; + shop_settings::Entity::set(&ctx.db, "bank_account_name", trimmed(&form.bank_account_name)).await?; + shop_settings::Entity::set(&ctx.db, "bank_iban", trimmed(&form.bank_iban)).await?; + format::redirect("/admin/payments") +} + +pub fn routes() -> Routes { + Routes::new() + .add("/admin/payments", get(index)) + .add("/admin/payments/methods/{id}", post(update_method)) + .add("/admin/payments/bank", post(update_bank)) +} diff --git a/src/controllers/checkout.rs b/src/controllers/checkout.rs index 52895f1..142a7c1 100644 --- a/src/controllers/checkout.rs +++ b/src/controllers/checkout.rs @@ -14,7 +14,7 @@ use crate::{ mailers::auth::AuthMailer, models::{ customer_profiles::{self, ProfileFields}, - order_items, orders, shipping_methods, + order_items, orders, payment_methods, shipping_methods, users::{self, normalize_account_type}, }, controllers::i18n::current_lang, @@ -22,8 +22,6 @@ use crate::{ views::checkout as view, }; -const PAYMENT_METHODS: [&str; 2] = ["cod", "bank_transfer"]; - #[derive(Debug, Deserialize)] struct CheckoutForm { email: String, @@ -76,6 +74,10 @@ async fn enabled_shipping_methods(ctx: &AppContext) -> Result Result> { + Ok(payment_methods::Entity::enabled(&ctx.db).await?) +} + #[debug_handler] async fn checkout_page( jar: CookieJar, @@ -102,6 +104,16 @@ async fn checkout_page( }) }) .collect(); + let payments: Vec = enabled_payment_methods(&ctx) + .await? + .iter() + .map(|m| { + json!({ + "code": m.code, + "label_key": m.label_key(), + }) + }) + .collect(); // Prefill the form for a logged-in customer: contact name/email come from // the user account, the address/phone from their saved profile (if any). @@ -130,6 +142,7 @@ async fn checkout_page( "subtotal": format_price(subtotal), "subtotal_cents": subtotal, "shipping_methods": methods, + "payment_methods": payments, "packeta_api_key": settings::get(&ctx, "packeta_api_key").unwrap_or(""), "logged_in_admin": is_admin, "logged_in_customer": is_customer, @@ -239,7 +252,7 @@ async fn place_order( (None, None, None, None) }; - if !PAYMENT_METHODS.contains(&form.payment_method.as_str()) { + if payment_methods::Entity::find_enabled(&ctx.db, &form.payment_method).await?.is_none() { return Err(Error::BadRequest("invalid payment method".to_string())); } @@ -392,14 +405,15 @@ async fn order_confirmation( let c = guard::chrome(&ctx, &jar).await; let account_created = params.contains_key("account_created"); + let (bank_iban, bank_account_name) = settings::bank_details(&ctx).await?; format::view( &v, "shop/order_confirmed.html", json!({ "order": view::detail( &order, - settings::get(&ctx, "bank_iban").unwrap_or(""), - settings::get(&ctx, "bank_account_name").unwrap_or(""), + &bank_iban, + &bank_account_name, ), "items": view::items(&items), "logged_in_admin": c.logged_in_admin, diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index fb88f6e..e73ead5 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -9,6 +9,7 @@ pub mod admin_dashboard; pub mod admin_discount_profiles; pub mod admin_form; pub mod admin_orders; +pub mod admin_payments; pub mod admin_products; pub mod admin_shipping; pub mod cart; diff --git a/src/initializers/mod.rs b/src/initializers/mod.rs index 4163c46..a3fb5d4 100644 --- a/src/initializers/mod.rs +++ b/src/initializers/mod.rs @@ -2,5 +2,6 @@ pub mod admin_seeder; pub mod currency_seeder; pub mod oauth2; pub mod oauth2_session; +pub mod payment_seeder; pub mod shipping_seeder; pub mod view_engine; diff --git a/src/initializers/payment_seeder.rs b/src/initializers/payment_seeder.rs new file mode 100644 index 0000000..4f10112 --- /dev/null +++ b/src/initializers/payment_seeder.rs @@ -0,0 +1,73 @@ +//! Ensures built-in payment methods and editable bank-transfer settings exist. +//! +//! Payment method enabled flags and bank account details are admin-managed in the +//! database. We seed missing rows only, so admin changes persist across restarts. + +use async_trait::async_trait; +use loco_rs::prelude::*; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; + +use crate::{ + models::{payment_methods, shop_settings}, + shared::settings, +}; + +/// `(code, name, enabled, position)` +const METHODS: [(&str, &str, bool, i32); 2] = [ + (payment_methods::COD, "Cash on delivery", true, 0), + (payment_methods::BANK_TRANSFER, "Bank transfer", true, 1), +]; + +pub struct PaymentSeeder; + +#[async_trait] +impl Initializer for PaymentSeeder { + fn name(&self) -> String { + "payment-seeder".to_string() + } + + async fn before_run(&self, ctx: &AppContext) -> Result<()> { + for (code, name, enabled, position) in METHODS { + let exists = payment_methods::Entity::find() + .filter(payment_methods::Column::Code.eq(code)) + .count(&ctx.db) + .await? + > 0; + if exists { + continue; + } + payment_methods::ActiveModel { + code: Set(code.to_string()), + name: Set(name.to_string()), + enabled: Set(enabled), + position: Set(position), + ..Default::default() + } + .insert(&ctx.db) + .await?; + tracing::info!(payment = code, "seeded built-in payment method"); + } + + seed_setting(ctx, "bank_iban").await?; + seed_setting(ctx, "bank_account_name").await + } +} + +async fn seed_setting(ctx: &AppContext, key: &str) -> Result<()> { + let exists = shop_settings::Entity::find() + .filter(shop_settings::Column::Key.eq(key)) + .count(&ctx.db) + .await? + > 0; + if exists { + return Ok(()); + } + shop_settings::ActiveModel { + key: Set(key.to_string()), + value: Set(settings::get(ctx, key).map(str::to_string)), + ..Default::default() + } + .insert(&ctx.db) + .await?; + Ok(()) +} diff --git a/src/models/_entities/mod.rs b/src/models/_entities/mod.rs index ba3e0b9..d0fd0fa 100644 --- a/src/models/_entities/mod.rs +++ b/src/models/_entities/mod.rs @@ -15,10 +15,12 @@ pub mod discount_profiles; pub mod o_auth2_sessions; pub mod order_items; pub mod orders; +pub mod payment_methods; pub mod product_images; pub mod product_product_tags; pub mod product_tags; pub mod product_variants; pub mod products; pub mod shipping_methods; +pub mod shop_settings; pub mod users; diff --git a/src/models/_entities/payment_methods.rs b/src/models/_entities/payment_methods.rs new file mode 100644 index 0000000..deb0c48 --- /dev/null +++ b/src/models/_entities/payment_methods.rs @@ -0,0 +1,21 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "payment_methods")] +pub struct Model { + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub code: String, + pub name: String, + pub enabled: bool, + pub position: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} diff --git a/src/models/_entities/prelude.rs b/src/models/_entities/prelude.rs index ee1e63a..911c273 100644 --- a/src/models/_entities/prelude.rs +++ b/src/models/_entities/prelude.rs @@ -13,10 +13,12 @@ pub use super::discount_profiles::Entity as DiscountProfiles; pub use super::o_auth2_sessions::Entity as OAuth2Sessions; pub use super::order_items::Entity as OrderItems; pub use super::orders::Entity as Orders; +pub use super::payment_methods::Entity as PaymentMethods; pub use super::product_images::Entity as ProductImages; pub use super::product_product_tags::Entity as ProductProductTags; pub use super::product_tags::Entity as ProductTags; pub use super::product_variants::Entity as ProductVariants; pub use super::products::Entity as Products; pub use super::shipping_methods::Entity as ShippingMethods; +pub use super::shop_settings::Entity as ShopSettings; pub use super::users::Entity as Users; diff --git a/src/models/_entities/shop_settings.rs b/src/models/_entities/shop_settings.rs new file mode 100644 index 0000000..a86fed3 --- /dev/null +++ b/src/models/_entities/shop_settings.rs @@ -0,0 +1,20 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "shop_settings")] +pub struct Model { + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub key: String, + #[sea_orm(column_type = "Text", nullable)] + pub value: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} diff --git a/src/models/mod.rs b/src/models/mod.rs index e9b6721..8bbc1aa 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -19,10 +19,12 @@ pub mod customer_profiles; pub mod o_auth2_sessions; pub mod order_items; pub mod orders; +pub mod payment_methods; pub mod product_images; pub mod product_product_tags; pub mod product_tags; pub mod products; pub mod shipping_methods; +pub mod shop_settings; pub mod users; pub mod product_variants; diff --git a/src/models/payment_methods.rs b/src/models/payment_methods.rs new file mode 100644 index 0000000..e13e5e3 --- /dev/null +++ b/src/models/payment_methods.rs @@ -0,0 +1,54 @@ +use sea_orm::entity::prelude::*; +use sea_orm::{ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; + +pub use crate::models::_entities::payment_methods::{ActiveModel, Column, Entity, Model}; +pub type PaymentMethods = Entity; + +pub const COD: &str = "cod"; +pub const BANK_TRANSFER: &str = "bank_transfer"; + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel { + async fn before_save(self, _db: &C, insert: bool) -> std::result::Result + where + C: ConnectionTrait, + { + if !insert && self.updated_at.is_unchanged() { + let mut this = self; + this.updated_at = ActiveValue::set(chrono::Utc::now().into()); + Ok(this) + } else { + Ok(self) + } + } +} + +impl Entity { + pub async fn enabled(db: &C) -> Result, DbErr> { + Entity::find() + .filter(Column::Enabled.eq(true)) + .order_by_asc(Column::Position) + .all(db) + .await + } + + pub async fn find_enabled(db: &C, code: &str) -> Result, DbErr> { + Entity::find() + .filter(Column::Code.eq(code)) + .filter(Column::Enabled.eq(true)) + .one(db) + .await + } +} + +impl Model { + pub fn label_key(&self) -> &'static str { + match self.code.as_str() { + COD => "payment-cod", + BANK_TRANSFER => "payment-bank", + _ => "payment-custom", + } + } +} + +impl ActiveModel {} diff --git a/src/models/shop_settings.rs b/src/models/shop_settings.rs new file mode 100644 index 0000000..8c63077 --- /dev/null +++ b/src/models/shop_settings.rs @@ -0,0 +1,47 @@ +use sea_orm::entity::prelude::*; +use sea_orm::{ActiveValue, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, TryIntoModel}; + +pub use crate::models::_entities::shop_settings::{ActiveModel, Column, Entity, Model}; +pub type ShopSettings = Entity; + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel { + async fn before_save(self, _db: &C, insert: bool) -> std::result::Result + where + C: ConnectionTrait, + { + if !insert && self.updated_at.is_unchanged() { + let mut this = self; + this.updated_at = ActiveValue::set(chrono::Utc::now().into()); + Ok(this) + } else { + Ok(self) + } + } +} + +impl Entity { + pub async fn get(db: &C, key: &str) -> Result, DbErr> { + Ok(Entity::find() + .filter(Column::Key.eq(key)) + .one(db) + .await? + .and_then(|setting| setting.value)) + } + + pub async fn set(db: &C, key: &str, value: Option) -> Result { + let mut active = match Entity::find() + .filter(Column::Key.eq(key)) + .one(db) + .await? + { + Some(existing) => existing.into_active_model(), + None => ActiveModel { + key: ActiveValue::set(key.to_string()), + ..Default::default() + }, + }; + active.value = ActiveValue::set(value); + active.save(db).await?.try_into_model() + } +} diff --git a/src/shared/settings.rs b/src/shared/settings.rs index e0965c0..ad33767 100644 --- a/src/shared/settings.rs +++ b/src/shared/settings.rs @@ -2,6 +2,8 @@ use loco_rs::prelude::*; +use crate::models::shop_settings; + /// Look up a string-valued `settings.` entry, returning `None` if config /// has no settings map, the key is missing, or the value is not a string. pub fn get<'a>(ctx: &'a AppContext, key: &str) -> Option<&'a str> { @@ -11,3 +13,20 @@ pub fn get<'a>(ctx: &'a AppContext, key: &str) -> Option<&'a str> { .and_then(|settings| settings.get(key)) .and_then(|value| value.as_str()) } + +/// Look up an admin-editable setting in the database, falling back to config when +/// the row is missing. Empty DB values are returned as-is so admins can clear a +/// setting deliberately. +pub async fn get_editable(ctx: &AppContext, key: &str) -> Result { + Ok(match shop_settings::Entity::get(&ctx.db, key).await? { + Some(value) => value, + None => get(ctx, key).unwrap_or("").to_string(), + }) +} + +pub async fn bank_details(ctx: &AppContext) -> Result<(String, String)> { + Ok(( + get_editable(ctx, "bank_iban").await?, + get_editable(ctx, "bank_account_name").await?, + )) +}