diff --git a/client b/client index b36ef1de..9dbcd82f 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit b36ef1decba9a4ed5e965938372ae72036b5566c +Subproject commit 9dbcd82f37a22f94fb7dce29dba49f13eaa8e530 diff --git a/common/proto/exchange_rates.proto b/common/proto/exchange_rates.proto index 07589265..bd9c82d8 100644 --- a/common/proto/exchange_rates.proto +++ b/common/proto/exchange_rates.proto @@ -22,6 +22,10 @@ message ExchangeRateSelection { optional string specific_rate_date = 3; optional string manual_foreign_units = 4; string reason = 5; + // Omit to use the profile's current default source for the currency. + // An explicit different source must be enabled for the same direct pair and + // is recorded as an audited per-event override. + optional string rate_source_id = 6; } service ExchangeRateService { @@ -31,7 +35,7 @@ service ExchangeRateService { returns (ListConversionEvidenceResponse); rpc GetProfileExchangeRateSettings(GetProfileExchangeRateSettingsRequest) returns (ProfileExchangeRateSettings); - rpc AddProfileForeignCurrency(AddProfileForeignCurrencyRequest) + rpc AddProfileCurrencySource(AddProfileCurrencySourceRequest) returns (ProfileForeignCurrency); } @@ -121,6 +125,12 @@ message GetProfileExchangeRateSettingsRequest { string profile_name = 1; } message ProfileForeignCurrency { string currency = 1; + string default_source_id = 2; + repeated ProfileCurrencySource sources = 3; +} + +message ProfileCurrencySource { + string source_id = 1; bool coverage_complete = 2; optional string verified_from_date = 3; optional string verified_through_date = 4; @@ -129,11 +139,13 @@ message ProfileForeignCurrency { message ProfileExchangeRateSettings { string profile_name = 1; string accounting_currency = 2; - string rate_source_id = 3; + repeated string enabled_source_ids = 3; repeated ProfileForeignCurrency foreign_currencies = 4; } -message AddProfileForeignCurrencyRequest { +message AddProfileCurrencySourceRequest { string profile_name = 1; string currency = 2; + string source_id = 3; + bool make_default = 4; } diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 59f52604..f0c79d1a 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -104,8 +104,8 @@ message PostTableDefinitionRequest { // to this one when they reach the ledger. string accounting_currency = 8; - // Compiled direct-rate provider selected when the profile is created. - // The server validates this identifier against its compiled provider registry. + // Initial compiled direct-rate provider. It is enabled for each initial + // foreign currency below; more provider/currency bindings may be appended later. string rate_source_id = 11; // Append-only foreign currencies initially enabled for this profile. diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 0b721938..9fe5cba2 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.exchange_rates.rs b/common/src/proto/komp_ac.exchange_rates.rs index 0ea8af54..56ea9ddf 100644 --- a/common/src/proto/komp_ac.exchange_rates.rs +++ b/common/src/proto/komp_ac.exchange_rates.rs @@ -11,6 +11,11 @@ pub struct ExchangeRateSelection { pub manual_foreign_units: ::core::option::Option<::prost::alloc::string::String>, #[prost(string, tag = "5")] pub reason: ::prost::alloc::string::String, + /// Omit to use the profile's current default source for the currency. + /// An explicit different source must be enabled for the same direct pair and + /// is recorded as an audited per-event override. + #[prost(string, optional, tag = "6")] + pub rate_source_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PreviewDirectConversionRequest { @@ -166,10 +171,19 @@ pub struct GetProfileExchangeRateSettingsRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, } -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ProfileForeignCurrency { #[prost(string, tag = "1")] pub currency: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub default_source_id: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "3")] + pub sources: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ProfileCurrencySource { + #[prost(string, tag = "1")] + pub source_id: ::prost::alloc::string::String, #[prost(bool, tag = "2")] pub coverage_complete: bool, #[prost(string, optional, tag = "3")] @@ -183,17 +197,21 @@ pub struct ProfileExchangeRateSettings { pub profile_name: ::prost::alloc::string::String, #[prost(string, tag = "2")] pub accounting_currency: ::prost::alloc::string::String, - #[prost(string, tag = "3")] - pub rate_source_id: ::prost::alloc::string::String, + #[prost(string, repeated, tag = "3")] + pub enabled_source_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(message, repeated, tag = "4")] pub foreign_currencies: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct AddProfileForeignCurrencyRequest { +pub struct AddProfileCurrencySourceRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, #[prost(string, tag = "2")] pub currency: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub source_id: ::prost::alloc::string::String, + #[prost(bool, tag = "4")] + pub make_default: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] @@ -445,9 +463,9 @@ pub mod exchange_rate_service_client { ); self.inner.unary(req, path, codec).await } - pub async fn add_profile_foreign_currency( + pub async fn add_profile_currency_source( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> std::result::Result< tonic::Response, tonic::Status, @@ -462,14 +480,14 @@ pub mod exchange_rate_service_client { })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( - "/komp_ac.exchange_rates.ExchangeRateService/AddProfileForeignCurrency", + "/komp_ac.exchange_rates.ExchangeRateService/AddProfileCurrencySource", ); let mut req = request.into_request(); req.extensions_mut() .insert( GrpcMethod::new( "komp_ac.exchange_rates.ExchangeRateService", - "AddProfileForeignCurrency", + "AddProfileCurrencySource", ), ); self.inner.unary(req, path, codec).await @@ -510,9 +528,9 @@ pub mod exchange_rate_service_server { tonic::Response, tonic::Status, >; - async fn add_profile_foreign_currency( + async fn add_profile_currency_source( &self, - request: tonic::Request, + request: tonic::Request, ) -> std::result::Result< tonic::Response, tonic::Status, @@ -750,16 +768,15 @@ pub mod exchange_rate_service_server { }; Box::pin(fut) } - "/komp_ac.exchange_rates.ExchangeRateService/AddProfileForeignCurrency" => { + "/komp_ac.exchange_rates.ExchangeRateService/AddProfileCurrencySource" => { #[allow(non_camel_case_types)] - struct AddProfileForeignCurrencySvc( + struct AddProfileCurrencySourceSvc( pub Arc, ); impl< T: ExchangeRateService, - > tonic::server::UnaryService< - super::AddProfileForeignCurrencyRequest, - > for AddProfileForeignCurrencySvc { + > tonic::server::UnaryService + for AddProfileCurrencySourceSvc { type Response = super::ProfileForeignCurrency; type Future = BoxFuture< tonic::Response, @@ -768,12 +785,12 @@ pub mod exchange_rate_service_server { fn call( &mut self, request: tonic::Request< - super::AddProfileForeignCurrencyRequest, + super::AddProfileCurrencySourceRequest, >, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::add_profile_foreign_currency( + ::add_profile_currency_source( &inner, request, ) @@ -788,7 +805,7 @@ pub mod exchange_rate_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = AddProfileForeignCurrencySvc(inner); + let method = AddProfileCurrencySourceSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( diff --git a/common/src/proto/komp_ac.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index c4456156..59af8e83 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -51,8 +51,8 @@ pub struct PostTableDefinitionRequest { /// to this one when they reach the ledger. #[prost(string, tag = "8")] pub accounting_currency: ::prost::alloc::string::String, - /// Compiled direct-rate provider selected when the profile is created. - /// The server validates this identifier against its compiled provider registry. + /// Initial compiled direct-rate provider. It is enabled for each initial + /// foreign currency below; more provider/currency bindings may be appended later. #[prost(string, tag = "11")] pub rate_source_id: ::prost::alloc::string::String, /// Append-only foreign currencies initially enabled for this profile. diff --git a/server b/server index d21be00a..0a8cf005 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit d21be00ac243bd9b16a855f9acab7dec5b2e88af +Subproject commit 0a8cf005d853b760eb295f57c223ea4f27cd42fc diff --git a/web/locales/cs/main.ftl b/web/locales/cs/main.ftl index ce656ed5..9cf8073c 100644 --- a/web/locales/cs/main.ftl +++ b/web/locales/cs/main.ftl @@ -507,7 +507,7 @@ grants-note-all-tables = Každá tabulka v každém profilu včetně těch, kter grants-label-all-journals = Všechny deníky grants-note-all-journals = Účetní deník každého profilu. grants-label-all-rates = Všechny devizové kurzy -grants-note-all-rates = Kurzy ECB importuje server, takže je lze pouze číst. +grants-note-all-rates = Oficiální kurzy importuje server, takže je lze pouze číst. grants-note-profile-tables = Každá tabulka v tomto profilu včetně těch, které přibudou později. grants-label-journal = Deník grants-note-journal = Účetní deník tohoto profilu. @@ -755,7 +755,7 @@ builder-choose-profile = Vyberte profil builder-new-profile = Nový profil… builder-new-profile-name = Název nového profilu builder-accounting-currency = Účetní měna -builder-accounting-currency-hint = Účetnictví může být vedeno v jakékoli měně, pro kterou ECB zveřejňuje referenční kurz. Jiná měna než EUR se v případě potřeby přepočítá přes EUR, protože kurzy ECB jsou vždy vyjádřeny vůči euru. +builder-accounting-currency-hint = Vybraný počáteční zdroj kurzů musí zveřejňovat přímý kurz mezi touto měnou a každou počáteční cizí měnou. Další zdroje a měny lze přidat později. builder-table-name = Název tabulky builder-add-column-heading = Přidat sloupec builder-add-column-button = Přidat sloupec diff --git a/web/locales/en/main.ftl b/web/locales/en/main.ftl index 8ecf2d68..0145edd9 100644 --- a/web/locales/en/main.ftl +++ b/web/locales/en/main.ftl @@ -500,7 +500,7 @@ grants-note-all-tables = Every table in every profile, including ones added late grants-label-all-journals = All journals grants-note-all-journals = Every profile's accounting journal. grants-label-all-rates = All exchange rates -grants-note-all-rates = ECB rates are imported by the server, so they can only be read. +grants-note-all-rates = Official rates are imported by the server, so they can only be read. grants-note-profile-tables = Every table in this profile, including ones added later. grants-label-journal = Journal grants-note-journal = This profile's accounting journal. @@ -743,7 +743,7 @@ builder-choose-profile = Choose a profile builder-new-profile = New profile… builder-new-profile-name = New profile name builder-accounting-currency = Accounting currency -builder-accounting-currency-hint = Books can be kept in any currency ECB publishes a reference rate for. A currency other than EUR is converted through EUR when needed, since ECB rates are always quoted against the euro. +builder-accounting-currency-hint = The selected initial rate source must publish a direct quote between this currency and every initial foreign currency. Additional sources and currencies can be added later. builder-table-name = Table name builder-add-column-heading = Add a column builder-add-column-button = Add column diff --git a/web/locales/sk/main.ftl b/web/locales/sk/main.ftl index 5949c941..480b1656 100644 --- a/web/locales/sk/main.ftl +++ b/web/locales/sk/main.ftl @@ -507,7 +507,7 @@ grants-note-all-tables = Každá tabuľka v každom profile vrátane tých, ktor grants-label-all-journals = Všetky denníky grants-note-all-journals = Účtovný denník každého profilu. grants-label-all-rates = Všetky devízové kurzy -grants-note-all-rates = Kurzy ECB importuje server, takže ich možno len čítať. +grants-note-all-rates = Oficiálne kurzy importuje server, takže ich možno len čítať. grants-note-profile-tables = Každá tabuľka v tomto profile vrátane tých, ktoré pribudnú neskôr. grants-label-journal = Denník grants-note-journal = Účtovný denník tohto profilu. @@ -753,7 +753,7 @@ builder-choose-profile = Vyberte profil builder-new-profile = Nový profil… builder-new-profile-name = Názov nového profilu builder-accounting-currency = Účtovná mena -builder-accounting-currency-hint = Účtovníctvo môže byť vedené v akejkoľvek mene, pre ktorú ECB zverejňuje referenčný kurz. Iná mena ako EUR sa v prípade potreby prepočíta cez EUR, keďže kurzy ECB sú vždy vyjadrené voči euru. +builder-accounting-currency-hint = Vybraný počiatočný zdroj kurzov musí zverejňovať priamy kurz medzi touto menou a každou počiatočnou cudzou menou. Ďalšie zdroje a meny možno pridať neskôr. builder-table-name = Názov tabuľky builder-add-column-heading = Pridať stĺpec builder-add-column-button = Pridať stĺpec diff --git a/web/src/authz.rs b/web/src/authz.rs index 42d43931..fe6ae853 100644 --- a/web/src/authz.rs +++ b/web/src/authz.rs @@ -9,9 +9,8 @@ pub(crate) const STRUCT_USER: &str = "struct:user"; pub(crate) const MANAGE: &str = "manage"; pub(crate) const READ: &str = "read"; -/// Every ECB object, which is what the pipeline status is checked against: -/// one importer feeds every profile, so reading its health is not a -/// per-profile question. +/// Every exchange-rate object. Provider pipeline health is a global rather +/// than per-profile concern. pub(crate) const ALL_EXCHANGE_RATES: &str = "exchange-rates:*"; /// The two bulk-transfer actions, mirroring @@ -43,7 +42,7 @@ pub(crate) fn can_manage(snapshot: &AuthorizationSnapshot, area: &str) -> bool { } /// Whether the caller may see the reference-rate pipeline. Mirrors the -/// server's own check in `server/src/ecb/grpc.rs`. +/// server's exchange-rate authorization check. pub(crate) fn can_read_exchange_rates(snapshot: &AuthorizationSnapshot) -> bool { permits(snapshot, ALL_EXCHANGE_RATES, READ) } diff --git a/web/src/pages/admin/admin/state.rs b/web/src/pages/admin/admin/state.rs index 4b3497fb..4fe30b4d 100644 --- a/web/src/pages/admin/admin/state.rs +++ b/web/src/pages/admin/admin/state.rs @@ -26,7 +26,7 @@ pub(crate) struct AdminPageState { pub can_import: bool, pub can_export: bool, /// Whether the exchange-rate pipeline is visible to this caller. Not a - /// structural area: it is granted through the ECB object, like the + /// structural area: it is granted through the exchange-rate object, like the /// conversions it reports on. pub can_exchange_rates: bool, } diff --git a/web/src/pages/permissions/grants/loader.rs b/web/src/pages/permissions/grants/loader.rs index 58922446..d292ba6c 100644 --- a/web/src/pages/permissions/grants/loader.rs +++ b/web/src/pages/permissions/grants/loader.rs @@ -181,7 +181,7 @@ fn describe(locale: Locale, object: &GrantableObject) -> (String, String) { tr!(locale, "grants-label-all-journals"), tr!(locale, "grants-note-all-journals"), ), - "global_ecb" => ( + "global_exchange_rates" => ( tr!(locale, "grants-label-all-rates"), tr!(locale, "grants-note-all-rates"), ), @@ -193,7 +193,7 @@ fn describe(locale: Locale, object: &GrantableObject) -> (String, String) { tr!(locale, "grants-label-journal"), tr!(locale, "grants-note-journal"), ), - "ecb" => ( + "exchange_rates" => ( tr!(locale, "grants-label-rates"), tr!(locale, "grants-note-rates"), ), diff --git a/web/src/pages/permissions/grants/state.rs b/web/src/pages/permissions/grants/state.rs index e905684e..83a3d1c1 100644 --- a/web/src/pages/permissions/grants/state.rs +++ b/web/src/pages/permissions/grants/state.rs @@ -229,7 +229,7 @@ mod tests { }, ObjectRow { object: "exchange-rates:acme".to_string(), - label: "ECB rates".to_string(), + label: "Exchange rates".to_string(), note: String::new(), wildcard: true, cells: cells(&["read"], &[]), @@ -249,7 +249,7 @@ mod tests { fn a_profile_shortcut_grants_the_wildcards_and_never_an_action_the_server_refuses() { let group = group(); // The table row is covered by data:acme/*, so it is not granted again, - // and ECB rates only ever offer read. + // and official rates only ever offer read. assert_eq!( group.all_pairs(), vec![ diff --git a/web/src/pages/permissions/roles/state.rs b/web/src/pages/permissions/roles/state.rs index d80b12ba..ed6be465 100644 --- a/web/src/pages/permissions/roles/state.rs +++ b/web/src/pages/permissions/roles/state.rs @@ -71,8 +71,8 @@ impl RoleRow { /// /// These are wildcard objects on purpose: they keep covering profiles and /// tables added later, which is what "everything" has to mean for a role -/// created before the data exists. ECB rates are written by the server, so they -/// are readable and nothing more. +/// created before the data exists. Official rates are written by the server, +/// so they are readable and nothing more. pub(crate) fn starter_grants( locale: Locale, access: &str, @@ -110,7 +110,7 @@ mod tests { use super::*; #[test] - fn starter_access_never_hands_out_a_write_on_ecb_rates() { + fn starter_access_never_hands_out_a_write_on_exchange_rates() { let locale = Locale::default(); assert!(starter_grants(locale, "none").unwrap().is_empty()); assert!(starter_grants(locale, "nonsense").is_err()); @@ -121,12 +121,12 @@ mod tests { grants .iter() .all(|(object, action)| *object != "exchange-rates:*" || *action == "read"), - "{level} granted a write on ECB rates" + "{level} granted a write on exchange rates" ); } assert_eq!(starter_grants(locale, "read").unwrap().len(), 3); - // Four row actions on two wildcard objects, ECB read, and the two + // Four row actions on two wildcard objects, rate read, and the two // transfer actions, which apply to tables alone. assert_eq!(starter_grants(locale, "full").unwrap().len(), 11); }