From 2e95201a714d4c9e1249c62957ebe7ee639ede41 Mon Sep 17 00:00:00 2001 From: Filipriec Date: Mon, 24 Aug 2026 07:03:57 +0200 Subject: [PATCH] exchange broker options --- common/proto/exchange_rates.proto | 14 +++ common/proto/table_definition.proto | 5 + common/src/proto/descriptor.bin | Bin 193520 -> 194486 bytes common/src/proto/komp_ac.exchange_rates.rs | 99 ++++++++++++++++++ common/src/proto/komp_ac.table_definition.rs | 5 + server | 2 +- web/CHANGELOG.md | 7 +- web/locales/cs/main.ftl | 11 +- web/locales/en/main.ftl | 11 +- web/locales/sk/main.ftl | 11 +- web/src/lib.rs | 6 +- web/src/pages/add_table/loader.rs | 14 +++ web/src/pages/add_table/logic.rs | 1 + web/src/pages/add_table/state.rs | 4 + web/src/pages/add_table/ui.rs | 16 ++- .../pages/admin/table_definition/loader.rs | 1 + web/src/pages/admin/table_definition/logic.rs | 24 +++-- web/src/pages/admin/table_definition/state.rs | 8 ++ web/src/pages/admin/table_definition/ui.rs | 16 ++- web/templates/pages/add_table/add_table.html | 3 + web/templates/pages/add_table/builder.html | 4 +- .../admin/table_definition/add_columns.html | 2 +- .../table_definition/add_columns_panel.html | 13 ++- .../admin/table_definition/column_panel.html | 2 +- .../pages/admin/table_definition/context.html | 6 +- 25 files changed, 246 insertions(+), 39 deletions(-) diff --git a/common/proto/exchange_rates.proto b/common/proto/exchange_rates.proto index 72ecd709..3234b96e 100644 --- a/common/proto/exchange_rates.proto +++ b/common/proto/exchange_rates.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package komp_ac.exchange_rates; +import "common.proto"; + // Selects WHICH DATE supplies the rate; this is independent of where the rate // comes from. These are not four user-facing choices: UNSPECIFIED is protobuf's // zero-value sentinel and is rejected when an explicit selection is supplied. @@ -67,6 +69,10 @@ message ExchangeRateSelection { } service ExchangeRateService { + // Lists the direct-rate providers compiled into and registered by this + // server instance. Clients use these stable IDs when configuring profiles. + rpc ListRateSources(komp_ac.common.Empty) + returns (ListRateSourcesResponse); rpc PreviewDirectConversion(PreviewDirectConversionRequest) returns (PreviewDirectConversionResponse); rpc ListConversionEvidence(ListConversionEvidenceRequest) @@ -77,6 +83,14 @@ service ExchangeRateService { returns (ProfileForeignCurrency); } +message RateSourceProvider { + string id = 1; +} + +message ListRateSourcesResponse { + repeated RateSourceProvider sources = 1; +} + message PreviewDirectConversionRequest { string original_amount = 1; string original_currency = 2; diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 076a1873..31a9fd16 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -443,6 +443,11 @@ message TableDetail { // requested profile when a global table is being listed. See // ProfileTreeResponse.Table.profile_name. string profile_name = 11; + + // True when the physical table contains at least one row, including a + // soft-deleted row. Populated tables are append-only: PutTableDefinition is + // no longer available, while AddTableColumns remains available. + bool has_data = 12; } // Server-owned behavior for one logical column returned in table details. diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index f45b2ca7ebcb90be6b4a6b959edb1b5c035afcfd..a71ec3aeddeb3fe899d12e620196c52279b3515e 100644 GIT binary patch delta 10156 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 I4gTr>0H^41PXGV_ delta 9213 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, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RateSourceProvider { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListRateSourcesResponse { + #[prost(message, repeated, tag = "1")] + pub sources: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PreviewDirectConversionRequest { #[prost(string, tag = "1")] pub original_amount: ::prost::alloc::string::String, @@ -408,6 +418,37 @@ pub mod exchange_rate_service_client { self.inner = self.inner.max_encoding_message_size(limit); self } + /// Lists the direct-rate providers compiled into and registered by this + /// server instance. Clients use these stable IDs when configuring profiles. + pub async fn list_rate_sources( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.exchange_rates.ExchangeRateService/ListRateSources", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.exchange_rates.ExchangeRateService", + "ListRateSources", + ), + ); + self.inner.unary(req, path, codec).await + } pub async fn preview_direct_conversion( &mut self, request: impl tonic::IntoRequest, @@ -541,6 +582,15 @@ pub mod exchange_rate_service_server { /// Generated trait containing gRPC methods that should be implemented for use with ExchangeRateServiceServer. #[async_trait] pub trait ExchangeRateService: std::marker::Send + std::marker::Sync + 'static { + /// Lists the direct-rate providers compiled into and registered by this + /// server instance. Clients use these stable IDs when configuring profiles. + async fn list_rate_sources( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; async fn preview_direct_conversion( &self, request: tonic::Request, @@ -646,6 +696,55 @@ pub mod exchange_rate_service_server { } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { + "/komp_ac.exchange_rates.ExchangeRateService/ListRateSources" => { + #[allow(non_camel_case_types)] + struct ListRateSourcesSvc(pub Arc); + impl< + T: ExchangeRateService, + > tonic::server::UnaryService + for ListRateSourcesSvc { + type Response = super::ListRateSourcesResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::list_rate_sources( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ListRateSourcesSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/komp_ac.exchange_rates.ExchangeRateService/PreviewDirectConversion" => { #[allow(non_camel_case_types)] struct PreviewDirectConversionSvc( diff --git a/common/src/proto/komp_ac.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index 2d84a993..7a498dba 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -458,6 +458,11 @@ pub struct TableDetail { /// ProfileTreeResponse.Table.profile_name. #[prost(string, tag = "11")] pub profile_name: ::prost::alloc::string::String, + /// True when the physical table contains at least one row, including a + /// soft-deleted row. Populated tables are append-only: PutTableDefinition is + /// no longer available, while AddTableColumns remains available. + #[prost(bool, tag = "12")] + pub has_data: bool, } /// Server-owned behavior for one logical column returned in table details. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] diff --git a/server b/server index f559f57d..fe581213 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit f559f57d40b9b0b8821ff9a924b2392903302813 +Subproject commit fe58121374af694f5d1cea0a6d1e82bad8511660 diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index f65757e6..b47a30c2 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -16,10 +16,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed +- **Backend-provided rate-source suggestions** — the add-table page calls + `ExchangeRateService.ListRateSources` and offers the returned provider IDs + through the rate-source autocomplete field; initial foreign currencies use + the same ISO-4217 autocomplete options as the accounting currency. - **Editable empty-table definitions** — the table-definition column page now calls `PutTableDefinition` to atomically remove selected columns and append replacements, with stable column identities and optimistic revision checks. - Add-only changes continue to work for populated tables. + It is clearly labelled “Adjust table” only while the table is empty; after + data is posted, the page switches to append-only “Add columns” mode. - **The shared profile is named by the backend** — `ProfileTreeResponse` gained `shared_profile_name`, and every page that browses the shared tables now reads the name from there instead of spelling `__global` itself. Catalog diff --git a/web/locales/cs/main.ftl b/web/locales/cs/main.ftl index 2320c00f..d29b367d 100644 --- a/web/locales/cs/main.ftl +++ b/web/locales/cs/main.ftl @@ -161,12 +161,12 @@ admin-system-columns-label = Systémové sloupce · skryté před uživateli # --- Stránky definice tabulky (přidat sloupce / zobrazení / smazat / kopírovat / šablona / historie) td-add-columns-title = Přidat sloupce -td-adjust-columns-title = Upravit sloupce +td-adjust-table-title = Upravit tabulku td-presentation-title = Zobrazení sloupců td-add-columns-to = Přidat sloupce do td-append-hint = Sloupce se přidávají na konec. Nic, co už existuje, se nemění a nové sloupce lze indexovat hned při přidání. -td-adjust-columns-of = Upravit sloupce tabulky -td-adjust-hint = Přidejte nové sloupce nebo označte existující k odebrání. Samotné přidání funguje i v tabulce s daty; odebrání nebo nahrazení vyžaduje prázdnou tabulku a uloží se atomicky. +td-adjust-table-of = Upravit tabulku +td-adjust-table-hint = Tato tabulka neobsahuje data, takže můžete přidat nové sloupce nebo označit existující k odebrání. Změna se uloží atomicky. td-existing-columns = Existující sloupce td-new-columns = Nové sloupce k přidání td-remove-column = Odebrat @@ -258,8 +258,8 @@ td-global-label = Globální — všechny profily td-tab-aria = akce s tabulkou td-tab-add-columns = Přidat sloupce td-tab-add-columns-sub = Připojit nové sloupce -td-tab-adjust-columns = Upravit sloupce -td-tab-adjust-columns-sub = Přidat, odebrat nebo nahradit +td-tab-adjust-table = Upravit tabulku +td-tab-adjust-table-sub = Přidat, odebrat nebo nahradit sloupce td-tab-presentation = Zobrazení td-tab-presentation-sub = Změnit aliasy a pořadí td-tab-delete = Smazat @@ -338,6 +338,7 @@ td-err-describe-column = Před přidáním popište alespoň jeden sloupec. td-err-describe-change = Označte sloupec k odebrání nebo připravte alespoň jeden sloupec k přidání. td-err-backend-no-columns = Backend nepřidal sloupce. td-err-backend-no-adjustment = Backend neupravil definici tabulky. +td-err-populated-append-only = Tato tabulka obsahuje data. Existující sloupce nelze odebrat; lze pouze přidat nové sloupce. td-err-choose-rename = Zadejte nový název sloupce. td-err-unknown-column = Tento sloupec už do této tabulky nepatří. Panel níže ukazuje tabulku, jaká je teď. td-err-backend-no-rename = Backend nepřejmenoval sloupec. diff --git a/web/locales/en/main.ftl b/web/locales/en/main.ftl index 249ddc62..134515b3 100644 --- a/web/locales/en/main.ftl +++ b/web/locales/en/main.ftl @@ -163,12 +163,12 @@ admin-system-columns-label = System columns · hidden from users # --- Table definition pages (add columns / presentation / delete / copy / template / history) - td-add-columns-title = Add columns -td-adjust-columns-title = Adjust columns +td-adjust-table-title = Adjust table td-presentation-title = Column presentation td-add-columns-to = Add columns to td-append-hint = Columns are appended. Nothing that already exists is changed, and the new columns can be indexed as they are added. -td-adjust-columns-of = Adjust columns of -td-adjust-hint = Add new columns, or select existing columns to remove. Add-only changes also work on populated tables; removing or replacing columns requires an empty table and is saved atomically. +td-adjust-table-of = Adjust table +td-adjust-table-hint = This table has no data, so you can add new columns or select existing columns to remove. The change is saved atomically. td-existing-columns = Existing columns td-new-columns = New columns to add td-remove-column = Remove @@ -258,8 +258,8 @@ td-global-label = Global — all profiles td-tab-aria = table actions td-tab-add-columns = Add columns td-tab-add-columns-sub = Append new columns -td-tab-adjust-columns = Adjust columns -td-tab-adjust-columns-sub = Add, remove, or replace columns +td-tab-adjust-table = Adjust table +td-tab-adjust-table-sub = Add, remove, or replace columns td-tab-presentation = Presentation td-tab-presentation-sub = Change aliases and order td-tab-delete = Delete @@ -334,6 +334,7 @@ td-err-describe-column = Describe at least one column before adding. td-err-describe-change = Select a column to remove or stage at least one column to add. td-err-backend-no-columns = The backend did not add the columns. td-err-backend-no-adjustment = The backend did not adjust the table definition. +td-err-populated-append-only = This table contains data. Existing columns cannot be removed; only new columns can be added. td-err-choose-rename = Type a new name for the column. td-err-unknown-column = That column is not part of this table any more. The panel below is the table as it now is. td-err-backend-no-rename = The backend did not rename the column. diff --git a/web/locales/sk/main.ftl b/web/locales/sk/main.ftl index dbb48c89..7f7a4859 100644 --- a/web/locales/sk/main.ftl +++ b/web/locales/sk/main.ftl @@ -161,12 +161,12 @@ admin-system-columns-label = Systémové stĺpce · skryté pred používateľmi # --- Stránky definície tabuľky (pridať stĺpce / zobrazenie / vymazať / kopírovať / šablóna / história) td-add-columns-title = Pridať stĺpce -td-adjust-columns-title = Upraviť stĺpce +td-adjust-table-title = Upraviť tabuľku td-presentation-title = Zobrazenie stĺpcov td-add-columns-to = Pridať stĺpce do td-append-hint = Stĺpce sa pridávajú na koniec. Nič, čo už existuje, sa nemení a nové stĺpce možno indexovať hneď pri pridaní. -td-adjust-columns-of = Upraviť stĺpce tabuľky -td-adjust-hint = Pridajte nové stĺpce alebo označte existujúce na odstránenie. Samotné pridanie funguje aj v tabuľke s údajmi; odstránenie alebo nahradenie vyžaduje prázdnu tabuľku a uloží sa atómovo. +td-adjust-table-of = Upraviť tabuľku +td-adjust-table-hint = Táto tabuľka neobsahuje údaje, takže môžete pridať nové stĺpce alebo označiť existujúce na odstránenie. Zmena sa uloží atómovo. td-existing-columns = Existujúce stĺpce td-new-columns = Nové stĺpce na pridanie td-remove-column = Odstrániť @@ -258,8 +258,8 @@ td-global-label = Globálne — všetky profily td-tab-aria = akcie s tabuľkou td-tab-add-columns = Pridať stĺpce td-tab-add-columns-sub = Pripojiť nové stĺpce -td-tab-adjust-columns = Upraviť stĺpce -td-tab-adjust-columns-sub = Pridať, odstrániť alebo nahradiť +td-tab-adjust-table = Upraviť tabuľku +td-tab-adjust-table-sub = Pridať, odstrániť alebo nahradiť stĺpce td-tab-presentation = Zobrazenie td-tab-presentation-sub = Zmeniť aliasy a poradie td-tab-delete = Vymazať @@ -338,6 +338,7 @@ td-err-describe-column = Pred pridaním popíšte aspoň jeden stĺpec. td-err-describe-change = Označte stĺpec na odstránenie alebo pripravte aspoň jeden stĺpec na pridanie. td-err-backend-no-columns = Backend nepridal stĺpce. td-err-backend-no-adjustment = Backend neupravil definíciu tabuľky. +td-err-populated-append-only = Táto tabuľka obsahuje údaje. Existujúce stĺpce nemožno odstrániť; možno iba pridať nové stĺpce. td-err-choose-rename = Zadajte nový názov stĺpca. td-err-unknown-column = Tento stĺpec už do tejto tabuľky nepatrí. Panel nižšie ukazuje tabuľku, aká je teraz. td-err-backend-no-rename = Backend nepremenoval stĺpec. diff --git a/web/src/lib.rs b/web/src/lib.rs index f51dbe74..62baea50 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -88,7 +88,8 @@ mod definitions { use auth::auth_service_client::AuthServiceClient; use definitions::{ - ecb, table_definition::table_definition_client::TableDefinitionClient, + ecb, exchange_rates::exchange_rate_service_client::ExchangeRateServiceClient, + table_definition::table_definition_client::TableDefinitionClient, table_script::table_script_client::TableScriptClient, table_structure::table_structure_service_client::TableStructureServiceClient, table_validation::table_validation_service_client::TableValidationServiceClient, @@ -113,6 +114,7 @@ pub(crate) struct AppState { validations: TableValidationServiceClient, tables_data: TablesDataClient, ecb: EcbServiceClient, + exchange_rates: ExchangeRateServiceClient, /// The imports this process is running, which the import page polls. Not a /// client: an import outlives the request that started it, so where it has /// got to has to live somewhere both the task and the next request can see. @@ -149,6 +151,7 @@ pub async fn serve() -> Result<(), Box> { validations: TableValidationServiceClient::new(channel.clone()), tables_data: TablesDataClient::new(channel.clone()), ecb: EcbServiceClient::new(channel.clone()), + exchange_rates: ExchangeRateServiceClient::new(channel.clone()), structures: TableStructureServiceClient::new(channel), imports: Default::default(), }; @@ -215,6 +218,7 @@ mod tests { validations: TableValidationServiceClient::new(channel.clone()), tables_data: TablesDataClient::new(channel.clone()), ecb: EcbServiceClient::new(channel.clone()), + exchange_rates: ExchangeRateServiceClient::new(channel.clone()), structures: TableStructureServiceClient::new(channel), imports: Default::default(), }) diff --git a/web/src/pages/add_table/loader.rs b/web/src/pages/add_table/loader.rs index 0b76930f..a5f38f25 100644 --- a/web/src/pages/add_table/loader.rs +++ b/web/src/pages/add_table/loader.rs @@ -40,6 +40,19 @@ pub(crate) async fn load_page( return Err(LoadError::Forbidden); } + let mut exchange_rates = state.exchange_rates.clone(); + let rate_sources = exchange_rates + .list_rate_sources( + authenticated_request(headers, Empty {}).map_err(|_| LoadError::Unauthenticated)?, + ) + .await + .map_err(|error| LoadError::Backend(error.message().to_string()))? + .into_inner() + .sources + .into_iter() + .map(|source| source.id) + .collect(); + let mut definitions = state.definitions; // What a column may be is the backend's to say; the picker and every rule // the draft applies are read from this. @@ -135,6 +148,7 @@ pub(crate) async fn load_page( .into_iter() .map(|profile| profile.name) .collect(), + rate_sources, draft, status, error, diff --git a/web/src/pages/add_table/logic.rs b/web/src/pages/add_table/logic.rs index 2564bf0d..17292719 100644 --- a/web/src/pages/add_table/logic.rs +++ b/web/src/pages/add_table/logic.rs @@ -249,6 +249,7 @@ mod tests { nav: crate::ui::Nav::default(), shared_profile: "__global".to_string(), profiles: vec!["billing".to_string()], + rate_sources: vec!["ecb".to_string()], draft, status: None, error: None, diff --git a/web/src/pages/add_table/state.rs b/web/src/pages/add_table/state.rs index 8870f363..1a286f5e 100644 --- a/web/src/pages/add_table/state.rs +++ b/web/src/pages/add_table/state.rs @@ -198,6 +198,8 @@ impl BuilderForm { pub(crate) struct AddTablePageState { pub nav: crate::ui::Nav, pub profiles: Vec, + /// Provider IDs returned by ExchangeRateService.ListRateSources. + pub rate_sources: Vec, /// The profile a global table lands in, as the profile tree reports it. /// The builder posts it as the created table's scope. pub shared_profile: String, @@ -511,6 +513,7 @@ mod tests { nav: crate::ui::Nav::default(), shared_profile: "__global".to_string(), profiles: Vec::new(), + rate_sources: vec!["ecb".to_string()], draft: posted_form().to_draft(), status: None, error: None, @@ -597,6 +600,7 @@ mod tests { nav: crate::ui::Nav::default(), shared_profile: "__global".to_string(), profiles: Vec::new(), + rate_sources: vec!["ecb".to_string()], draft, status: None, error: None, diff --git a/web/src/pages/add_table/ui.rs b/web/src/pages/add_table/ui.rs index a64475f5..bb8dea0f 100644 --- a/web/src/pages/add_table/ui.rs +++ b/web/src/pages/add_table/ui.rs @@ -111,6 +111,7 @@ mod tests { nav: Nav::default(), shared_profile: "__global".to_string(), profiles: vec!["billing".to_string()], + rate_sources: vec!["ecb".to_string()], draft, status: None, error: None, @@ -221,13 +222,15 @@ mod tests { } #[test] - fn the_page_offers_the_new_profile_option_and_the_currency_list() { + fn the_page_offers_backend_vocabularies_for_profile_setup() { let html = render_page(&page()); assert!(!html.contains("Template error"), "{html}"); assert!(html.contains(r#"value="__new__""#)); assert!(html.contains(r#""#)); assert!(html.contains(r#"