diff --git a/client b/client index 78049ab8..097aba8b 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 78049ab882a32270bf0c976fd9ecef0a4409ee97 +Subproject commit 097aba8b61146e44161aeaf01886b7c95218abd4 diff --git a/common/build.rs b/common/build.rs index 29a3faaf..4d05e10a 100644 --- a/common/build.rs +++ b/common/build.rs @@ -233,6 +233,7 @@ fn main() -> Result<(), Box> { "proto/common.proto", "proto/document_data.proto", "proto/ecb.proto", + "proto/exchange_rates.proto", "proto/analytics.proto", "proto/auth.proto", "proto/backup.proto", @@ -259,6 +260,7 @@ fn main() -> Result<(), Box> { "proto/common.proto", "proto/document_data.proto", "proto/ecb.proto", + "proto/exchange_rates.proto", "proto/analytics.proto", "proto/auth.proto", "proto/backup.proto", diff --git a/common/proto/accounting.proto b/common/proto/accounting.proto index faf26c40..e10f6f09 100644 --- a/common/proto/accounting.proto +++ b/common/proto/accounting.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package komp_ac.accounting; -import "ecb.proto"; +import "exchange_rates.proto"; // Mutable informational journals. A journal may contain only debits, only // credits, or any non-zero balance. Balance is reported but never enforced. @@ -167,7 +167,7 @@ message JournalLineInput { string description = 4; // Empty uses previous publication and ECB. Set this per line when the // accountant needs a different rule, a saved custom rate, or a one-off rate. - optional komp_ac.ecb.ExchangeRateSelection exchange_rate_selection = 5; + optional komp_ac.exchange_rates.ExchangeRateSelection exchange_rate_selection = 5; } message CloseJournalRequest { diff --git a/common/proto/ecb.proto b/common/proto/ecb.proto index b0b909d3..826fd3c1 100644 --- a/common/proto/ecb.proto +++ b/common/proto/ecb.proto @@ -2,293 +2,46 @@ syntax = "proto3"; package komp_ac.ecb; -// How the publication date is selected from the accounting basis date. -enum ExchangeRateDateRule { - EXCHANGE_RATE_DATE_RULE_UNSPECIFIED = 0; - // Latest TARGET publication strictly before the basis date. - EXCHANGE_RATE_DATE_RULE_PREVIOUS_PUBLICATION = 1; - // Latest TARGET publication on or before the basis date. - EXCHANGE_RATE_DATE_RULE_ON_OR_BEFORE_DATE = 2; - // The exact publication date supplied in specific_rate_date. - EXCHANGE_RATE_DATE_RULE_SPECIFIC_PUBLICATION_DATE = 3; -} - -enum ExchangeRateSource { - EXCHANGE_RATE_SOURCE_UNSPECIFIED = 0; - // The rate source's own published reference rate (ECB's, today). - EXCHANGE_RATE_SOURCE_OFFICIAL = 1; - // A reusable rate from the profile's custom_exchange_rates table. - EXCHANGE_RATE_SOURCE_SAVED_CUSTOM = 2; - // A one-off rate recorded only in this posting's immutable evidence. - EXCHANGE_RATE_SOURCE_MANUAL = 3; -} - -// Accountant-controlled conversion treatment. Exceptional choices remain -// explicit and auditable rather than being inferred from a legal label. -message ExchangeRateSelection { - ExchangeRateDateRule date_rule = 1; - ExchangeRateSource source = 2; - // Required for SPECIFIC_PUBLICATION_DATE and MANUAL; YYYY-MM-DD. - optional string specific_rate_date = 3; - // Required only for MANUAL. Exact foreign-currency units per one unit of - // the rate source's pivot currency (EUR for ECB). - optional string manual_units_per_pivot = 4; - // Required for SPECIFIC_PUBLICATION_DATE and MANUAL. - string reason = 5; -} - -// Read-only access to ECB conversion previews and audit evidence. -// -// This service exists for manual visual verification before and after posting. -// Preview performs the authoritative unrounded backend calculation without -// writing; evidence exposes conversions already recorded by posting workflows. +// ECB-only operational details. Conversion selection and evidence live in the +// provider-neutral exchange_rates contract. service EcbService { - // Preview the exact conversion the backend would currently perform. - // This reads local verified ECB data and never persists conversion evidence. - rpc PreviewEcbConversion(PreviewEcbConversionRequest) - returns (PreviewEcbConversionResponse); - - // List immutable ECB conversion evidence for one money value. - rpc ListEcbConversionEvidence(ListEcbConversionEvidenceRequest) - returns (ListEcbConversionEvidenceResponse); - - // Health of the reference-rate import pipeline: how far verified coverage - // reaches, how far it should reach by now, and how the recent import - // attempts went. Not profile-scoped -- one pipeline feeds every profile. rpc GetEcbPipelineStatus(GetEcbPipelineStatusRequest) returns (GetEcbPipelineStatusResponse); } -message GetEcbPipelineStatusRequest { - // Optional. How many recent import attempts to return. Zero uses 20; the - // maximum is 100. - int32 batch_limit = 1; -} +message GetEcbPipelineStatusRequest { int32 batch_limit = 1; } -// One row of the append-only import audit log. message EcbImportBatch { int64 batch_id = 1; - // "running", "succeeded" or "failed". string status = 2; string requested_from = 3; string requested_through = 4; string endpoint = 5; string started_at = 6; optional string completed_at = 7; - // Present on a batch that advanced coverage. optional string verified_through_date = 8; optional int32 observation_count = 9; - // May be lower than observation_count: an observation already recorded by an - // earlier batch is kept rather than replaced. optional int32 inserted_observation_count = 10; optional string error_message = 11; } +message EcbCurrencyCoverage { + string currency = 1; + string status = 2; + optional string verified_from_date = 3; + optional string verified_through_date = 4; +} + message GetEcbPipelineStatusResponse { - // Latest date a successful batch verified. Absent when nothing has ever - // succeeded, which is what a pipeline that has never run looks like. optional string verified_through_date = 1; - - // The date coverage should have reached by now, derived from the same - // publication rule the importer schedules against. Comparing the two is what - // "healthy" means. string latest_verifiable_date = 2; - - // True when verified coverage has reached latest_verifiable_date. A - // conversion whose publication date falls beyond coverage is refused, so - // this answers "will posting work right now". bool healthy = 3; - - // Publication days between coverage and latest_verifiable_date. Zero when - // healthy. int32 days_behind = 4; - - // True while a batch holds the single-running lock. bool import_running = 5; - - // When the scheduler next wakes, from the same cutoff the importer uses. string next_import_at = 6; - - // Newest first, running and failed attempts included. repeated EcbImportBatch batches = 7; - - // Distinct currencies observed on verified_through_date, so a coverage gap - // for one currency is visible even when the date itself is covered. repeated string covered_currencies = 8; - - // Current coverage restated as the question a reader actually has: what may - // be posted right now. Both are the latest basis date whose derived - // publication date is still covered, under the two rules - // required_publication_date applies -- transactions look strictly before the - // basis date, statements and decisive dates look at it directly, so the - // transaction bound is the later of the two. Derived here rather than by the - // caller so it cannot disagree with the calendar the conversion path uses. - // Absent when nothing has ever been verified and nothing can be posted. optional string transactions_postable_through = 9; optional string statements_postable_through = 10; -} - -// Exact inputs for a conversion preview. Decimal values are strings so the -// client never loses precision through binary floating point. -message PreviewEcbConversionRequest { - string original_amount = 1; - string original_currency = 2; - - // ISO calendar date (YYYY-MM-DD) of the accounting case. - string conversion_basis_date = 3; - - // Required. Profile whose rates apply. Consulted when the selection requests - // a saved custom rate, and always required so the preview names its books. - string profile_name = 4; - - // Empty uses previous publication and ECB. - optional ExchangeRateSelection exchange_rate_selection = 5; -} - -// The conversion and immutable local ECB observation that would be used now. -// No rounding is performed and no conversion evidence is persisted. -message PreviewEcbConversionResponse { - string original_amount = 1; - string original_currency = 2; - string converted_amount = 3; - string conversion_basis_date = 4; - string determination_method = 5; - string rounding_method = 6; - optional string rate_date = 7; - optional string units_per_pivot = 8; - optional int64 rate_observation_id = 9; - optional string observation_hash = 10; - optional string source_payload_hash = 11; - optional string rate_fetched_at = 12; - optional int64 import_batch_id = 13; - optional string source_endpoint = 14; - - // Present only when determination_method is custom_rate. rate_date and - // units_per_pivot then describe the hand-entered row identified here, and - // the ECB observation fields above are empty. - optional int64 custom_rate_table_definition_id = 15; - optional int64 custom_rate_record_id = 16; - optional int64 custom_rate_row_revision = 17; - optional string custom_rate_note = 18; - - // Accountant-supplied justification for an exceptional publication date or - // one-off manual rate. Empty for the normal/default rule. - ExchangeRateDateRule applied_date_rule = 19; - ExchangeRateSource applied_source = 20; - string selection_reason = 21; - - // The currency converted_amount is stated in: the profile's accounting - // currency (EUR unless the profile keeps its books in something else). - string converted_currency = 22; - - // True when reaching converted_currency needed two hops (original currency - // to EUR, then EUR to converted_currency) because neither is EUR. The - // fields above then describe only the second, final hop; the full - // provenance of both hops is available from ListEcbConversionEvidence - // after posting. - bool via_two_hop_conversion = 23; -} - -// Identify one logical money cell whose conversion history should be inspected. -message ListEcbConversionEvidenceRequest { - // Required. Profile containing the table. - string profile_name = 1; - - // Required. Logical table name within the profile. - string table_name = 2; - - // Required. Dynamic table row id. - int64 record_id = 3; - - // Required. Current display name of the money column. The server resolves it - // to the stable physical column name used by immutable evidence. - string column_name = 4; - - // Optional. Maximum rows to return. Zero uses 100; maximum is 500. - int32 limit = 5; - - // Optional cursor. Return evidence ids lower than this value. - optional int64 before_evidence_id = 6; -} - -// One immutable explanation of how a posted EUR value was determined. -message EcbConversionEvidence { - int64 evidence_id = 1; - - // Exact committed version of the target dynamic-table row. - int64 row_revision = 2; - string original_amount = 3; - string original_currency = 4; - string converted_amount = 5; - string conversion_context = 6; - string conversion_basis_date = 7; - string determination_method = 8; - string rounding_method = 9; - optional string rate_date = 10; - optional string units_per_pivot = 11; - optional int64 rate_observation_id = 12; - optional string observation_hash = 13; - optional string source_payload_hash = 14; - optional string rate_fetched_at = 15; - optional int64 import_batch_id = 16; - optional string source_endpoint = 17; - string evidence_created_at = 18; - - // Present only when determination_method is custom_rate. The hand-entered row - // that supplied rate_date and units_per_pivot. The row itself stays editable, so - // the figures above are the immutable record of what was actually applied. - optional int64 custom_rate_table_definition_id = 19; - optional int64 custom_rate_record_id = 20; - optional int64 custom_rate_row_revision = 21; - optional string custom_rate_note = 22; - - // Immutable audit data for an accountant-controlled exception. - string selection_reason = 23; - optional string selected_by_user_id = 24; - - // The currency converted_amount is stated in. Always EUR for the - // original-currency-to-EUR leg; the profile's native currency for the - // EUR-to-native leg that follows it when that native currency is not EUR. - string converted_currency = 25; - - // Set only on the EUR-to-native-currency leg of a two-hop conversion. Names - // the original-currency-to-EUR leg's evidence_id, which produced this row's - // original_amount/original_currency (always EUR). - optional int64 derived_from_evidence_id = 26; - - // The pivot currency of the rate source that priced this hop (EUR for - // every hop today). A rate-based hop always has this currency on exactly - // one side of original_currency/converted_currency. - string pivot_currency = 27; - - // Which rate source's policy governs this hop's profile ("ecb" for every - // hop today). Absent only for already_same_currency, where no policy - // needed to be consulted. This is about which policy applies, not about - // who actually supplied this specific rate -- see observation_source_id - // for that. - optional string policy_source_id = 28; - - // Which rate source actually supplied this hop's rate as its own official - // observation. Present only when determination_method is - // official_reference_rate; absent for custom_rate, manual_rate, and - // already_same_currency, since none of those were priced by the policy's - // source itself -- a human entered them. - optional string observation_source_id = 29; - - // Exact immutable profile policy version selected for this conversion's - // accounting date. Absent only when no conversion policy was needed because - // determination_method is already_same_currency. - optional int64 rate_source_policy_id = 30; -} - -message ListEcbConversionEvidenceResponse { - // True when at least one evidence row for this cell used an ECB, saved - // custom, or one-off manual rate. - bool has_exchange = 1; - - // Newest-first immutable history for this page. - repeated EcbConversionEvidence evidence = 2; - - // True when another page exists using the last evidence_id as the cursor. - bool has_more = 3; + repeated EcbCurrencyCoverage currency_coverage = 11; } diff --git a/common/proto/exchange_rates.proto b/common/proto/exchange_rates.proto new file mode 100644 index 00000000..ac377d5e --- /dev/null +++ b/common/proto/exchange_rates.proto @@ -0,0 +1,139 @@ +syntax = "proto3"; + +package komp_ac.exchange_rates; + +enum ExchangeRateDateRule { + EXCHANGE_RATE_DATE_RULE_UNSPECIFIED = 0; + EXCHANGE_RATE_DATE_RULE_PREVIOUS_PUBLICATION = 1; + EXCHANGE_RATE_DATE_RULE_ON_OR_BEFORE_DATE = 2; + EXCHANGE_RATE_DATE_RULE_SPECIFIC_PUBLICATION_DATE = 3; +} + +enum ExchangeRateSource { + EXCHANGE_RATE_SOURCE_UNSPECIFIED = 0; + EXCHANGE_RATE_SOURCE_OFFICIAL = 1; + EXCHANGE_RATE_SOURCE_SAVED_CUSTOM = 2; + EXCHANGE_RATE_SOURCE_MANUAL = 3; +} + +message ExchangeRateSelection { + ExchangeRateDateRule date_rule = 1; + ExchangeRateSource source = 2; + optional string specific_rate_date = 3; + optional string manual_foreign_units = 4; + string reason = 5; +} + +service ExchangeRateService { + rpc PreviewDirectConversion(PreviewDirectConversionRequest) + returns (PreviewDirectConversionResponse); + rpc ListConversionEvidence(ListConversionEvidenceRequest) + returns (ListConversionEvidenceResponse); + rpc GetProfileExchangeRateSettings(GetProfileExchangeRateSettingsRequest) + returns (ProfileExchangeRateSettings); + rpc AddProfileForeignCurrency(AddProfileForeignCurrencyRequest) + returns (ProfileForeignCurrency); +} + +message PreviewDirectConversionRequest { + string original_amount = 1; + string original_currency = 2; + string conversion_basis_date = 3; + string profile_name = 4; + optional ExchangeRateSelection exchange_rate_selection = 5; +} + +message PreviewDirectConversionResponse { + string original_amount = 1; + string original_currency = 2; + string converted_amount = 3; + string accounting_currency = 4; + string conversion_basis_date = 5; + string determination_method = 6; + string rounding_method = 7; + optional string rate_date = 8; + optional string accounting_units = 9; + optional string foreign_currency = 10; + optional string foreign_units = 11; + string source_id = 12; + optional int64 rate_observation_id = 13; + optional string observation_hash = 14; + optional string source_payload_hash = 15; + optional string rate_fetched_at = 16; + optional int64 import_batch_id = 17; + optional string source_endpoint = 18; + optional int64 custom_rate_table_definition_id = 19; + optional int64 custom_rate_record_id = 20; + optional int64 custom_rate_row_revision = 21; + optional string custom_rate_note = 22; + ExchangeRateDateRule applied_date_rule = 23; + ExchangeRateSource applied_source = 24; + string selection_reason = 25; +} + +message ListConversionEvidenceRequest { + string profile_name = 1; + string table_name = 2; + int64 record_id = 3; + string column_name = 4; + int32 limit = 5; + optional int64 before_evidence_id = 6; +} + +message ConversionEvidence { + int64 evidence_id = 1; + int64 row_revision = 2; + string original_amount = 3; + string original_currency = 4; + string converted_amount = 5; + string accounting_currency = 6; + string accounting_units = 7; + string foreign_currency = 8; + string foreign_units = 9; + string conversion_context = 10; + string conversion_basis_date = 11; + string determination_method = 12; + string rounding_method = 13; + optional string rate_date = 14; + string source_id = 15; + optional int64 rate_observation_id = 16; + optional string observation_hash = 17; + optional string source_payload_hash = 18; + optional string rate_fetched_at = 19; + optional int64 import_batch_id = 20; + optional string source_endpoint = 21; + string evidence_created_at = 22; + optional int64 custom_rate_table_definition_id = 23; + optional int64 custom_rate_record_id = 24; + optional int64 custom_rate_row_revision = 25; + optional string custom_rate_note = 26; + string selection_reason = 27; + optional string selected_by_user_id = 28; +} + +message ListConversionEvidenceResponse { + bool has_exchange = 1; + repeated ConversionEvidence evidence = 2; + bool has_more = 3; +} + +message GetProfileExchangeRateSettingsRequest { string profile_name = 1; } + +message ProfileForeignCurrency { + string currency = 1; + bool coverage_complete = 2; + optional string verified_from_date = 3; + optional string verified_through_date = 4; +} + +message ProfileExchangeRateSettings { + string profile_name = 1; + string accounting_currency = 2; + string rate_source_id = 3; + repeated ProfileForeignCurrency foreign_currencies = 4; +} + +message AddProfileForeignCurrencyRequest { + string profile_name = 1; + string currency = 2; +} diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index afc76d58..138dc008 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -104,6 +104,13 @@ message PostTableDefinitionRequest { // to this one when they reach the ledger. string accounting_currency = 8; + // Compiled direct-rate provider selected when the profile is created. + // Currently only "ecb" is available, and it requires EUR accounting. + string rate_source_id = 11; + + // Append-only foreign currencies initially enabled for this profile. + repeated string foreign_currencies = 12; + // When true, the table is stored once in the global physical schema and is // visible from every profile. profile_name and accounting_currency are ignored. bool global = 9; diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto index c100f808..edad66df 100644 --- a/common/proto/tables_data.proto +++ b/common/proto/tables_data.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package komp_ac.tables_data; import "common.proto"; -import "ecb.proto"; +import "exchange_rates.proto"; import "google/protobuf/struct.proto"; // Read and write row data for user-defined tables inside profiles (schemas). @@ -166,7 +166,7 @@ map data = 3; message PostAccountingTableDataRequest { PostTableDataRequest row = 1; - komp_ac.ecb.ExchangeRateSelection exchange_rate_selection = 2; + komp_ac.exchange_rates.ExchangeRateSelection exchange_rate_selection = 2; } // Insert response. @@ -259,7 +259,7 @@ message PutTableDataRequest { message PutAccountingTableDataRequest { PutTableDataRequest update = 1; - komp_ac.ecb.ExchangeRateSelection exchange_rate_selection = 2; + komp_ac.exchange_rates.ExchangeRateSelection exchange_rate_selection = 2; bool confirmed = 3; repeated string expected_affected_profiles = 4; } diff --git a/common/src/lib.rs b/common/src/lib.rs index 004b5fd1..5eda2fa7 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -31,6 +31,9 @@ pub mod proto { pub mod ecb { include!("proto/komp_ac.ecb.rs"); } + pub mod exchange_rates { + include!("proto/komp_ac.exchange_rates.rs"); + } pub mod table_structure { include!("proto/komp_ac.table_structure.rs"); } diff --git a/common/src/money.rs b/common/src/money.rs index e092cc95..dd479191 100644 --- a/common/src/money.rs +++ b/common/src/money.rs @@ -15,14 +15,6 @@ pub use rusty_money::iso; /// whitespace, and any length but three. Normalising here instead would let a /// value be written in one spelling and compared in another. pub fn require_iso_currency_code(currency_code: &str) -> Result<&'static iso::Currency, String> { - if currency_code.len() != 3 - || !currency_code - .chars() - .all(|character| character.is_ascii_uppercase()) - { - return Err("Currency must be a canonical uppercase ISO-4217 code".to_string()); - } - iso::find(currency_code).ok_or_else(|| format!("Unknown ISO-4217 currency: {currency_code}")) } @@ -42,7 +34,18 @@ mod tests { #[test] fn non_canonical_or_unknown_codes_are_rejected() { for currency_code in [ - "", "E", "EU", "EURO", "eur", "Eur", " EUR", "EUR ", "E R", "E\u{20AC}R", "AAA", "123", + "", + "E", + "EU", + "EURO", + "eur", + "Eur", + " EUR", + "EUR ", + "E R", + "E\u{20AC}R", + "AAA", + "123", ] { assert!( require_iso_currency_code(currency_code).is_err(), diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index ae11d9a1..41a042a7 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.accounting.rs b/common/src/proto/komp_ac.accounting.rs index f7b8353a..b59eec42 100644 --- a/common/src/proto/komp_ac.accounting.rs +++ b/common/src/proto/komp_ac.accounting.rs @@ -72,7 +72,7 @@ pub struct JournalLineInput { /// accountant needs a different rule, a saved custom rate, or a one-off rate. #[prost(message, optional, tag = "5")] pub exchange_rate_selection: ::core::option::Option< - super::ecb::ExchangeRateSelection, + super::exchange_rates::ExchangeRateSelection, >, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] diff --git a/common/src/proto/komp_ac.ecb.rs b/common/src/proto/komp_ac.ecb.rs index 313dddc1..2d604b7b 100644 --- a/common/src/proto/komp_ac.ecb.rs +++ b/common/src/proto/komp_ac.ecb.rs @@ -1,36 +1,13 @@ // This file is @generated by prost-build. -/// Accountant-controlled conversion treatment. Exceptional choices remain -/// explicit and auditable rather than being inferred from a legal label. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ExchangeRateSelection { - #[prost(enumeration = "ExchangeRateDateRule", tag = "1")] - pub date_rule: i32, - #[prost(enumeration = "ExchangeRateSource", tag = "2")] - pub source: i32, - /// Required for SPECIFIC_PUBLICATION_DATE and MANUAL; YYYY-MM-DD. - #[prost(string, optional, tag = "3")] - pub specific_rate_date: ::core::option::Option<::prost::alloc::string::String>, - /// Required only for MANUAL. Exact foreign-currency units per one unit of - /// the rate source's pivot currency (EUR for ECB). - #[prost(string, optional, tag = "4")] - pub manual_units_per_pivot: ::core::option::Option<::prost::alloc::string::String>, - /// Required for SPECIFIC_PUBLICATION_DATE and MANUAL. - #[prost(string, tag = "5")] - pub reason: ::prost::alloc::string::String, -} #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetEcbPipelineStatusRequest { - /// Optional. How many recent import attempts to return. Zero uses 20; the - /// maximum is 100. #[prost(int32, tag = "1")] pub batch_limit: i32, } -/// One row of the append-only import audit log. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct EcbImportBatch { #[prost(int64, tag = "1")] pub batch_id: i64, - /// "running", "succeeded" or "failed". #[prost(string, tag = "2")] pub status: ::prost::alloc::string::String, #[prost(string, tag = "3")] @@ -43,59 +20,44 @@ pub struct EcbImportBatch { pub started_at: ::prost::alloc::string::String, #[prost(string, optional, tag = "7")] pub completed_at: ::core::option::Option<::prost::alloc::string::String>, - /// Present on a batch that advanced coverage. #[prost(string, optional, tag = "8")] pub verified_through_date: ::core::option::Option<::prost::alloc::string::String>, #[prost(int32, optional, tag = "9")] pub observation_count: ::core::option::Option, - /// May be lower than observation_count: an observation already recorded by an - /// earlier batch is kept rather than replaced. #[prost(int32, optional, tag = "10")] pub inserted_observation_count: ::core::option::Option, #[prost(string, optional, tag = "11")] pub error_message: ::core::option::Option<::prost::alloc::string::String>, } +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct EcbCurrencyCoverage { + #[prost(string, tag = "1")] + pub currency: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub status: ::prost::alloc::string::String, + #[prost(string, optional, tag = "3")] + pub verified_from_date: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "4")] + pub verified_through_date: ::core::option::Option<::prost::alloc::string::String>, +} #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetEcbPipelineStatusResponse { - /// Latest date a successful batch verified. Absent when nothing has ever - /// succeeded, which is what a pipeline that has never run looks like. #[prost(string, optional, tag = "1")] pub verified_through_date: ::core::option::Option<::prost::alloc::string::String>, - /// The date coverage should have reached by now, derived from the same - /// publication rule the importer schedules against. Comparing the two is what - /// "healthy" means. #[prost(string, tag = "2")] pub latest_verifiable_date: ::prost::alloc::string::String, - /// True when verified coverage has reached latest_verifiable_date. A - /// conversion whose publication date falls beyond coverage is refused, so - /// this answers "will posting work right now". #[prost(bool, tag = "3")] pub healthy: bool, - /// Publication days between coverage and latest_verifiable_date. Zero when - /// healthy. #[prost(int32, tag = "4")] pub days_behind: i32, - /// True while a batch holds the single-running lock. #[prost(bool, tag = "5")] pub import_running: bool, - /// When the scheduler next wakes, from the same cutoff the importer uses. #[prost(string, tag = "6")] pub next_import_at: ::prost::alloc::string::String, - /// Newest first, running and failed attempts included. #[prost(message, repeated, tag = "7")] pub batches: ::prost::alloc::vec::Vec, - /// Distinct currencies observed on verified_through_date, so a coverage gap - /// for one currency is visible even when the date itself is covered. #[prost(string, repeated, tag = "8")] pub covered_currencies: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - /// Current coverage restated as the question a reader actually has: what may - /// be posted right now. Both are the latest basis date whose derived - /// publication date is still covered, under the two rules - /// required_publication_date applies -- transactions look strictly before the - /// basis date, statements and decisive dates look at it directly, so the - /// transaction bound is the later of the two. Derived here rather than by the - /// caller so it cannot disagree with the calendar the conversion path uses. - /// Absent when nothing has ever been verified and nothing can be posted. #[prost(string, optional, tag = "9")] pub transactions_postable_through: ::core::option::Option< ::prost::alloc::string::String, @@ -104,292 +66,8 @@ pub struct GetEcbPipelineStatusResponse { pub statements_postable_through: ::core::option::Option< ::prost::alloc::string::String, >, -} -/// Exact inputs for a conversion preview. Decimal values are strings so the -/// client never loses precision through binary floating point. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct PreviewEcbConversionRequest { - #[prost(string, tag = "1")] - pub original_amount: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub original_currency: ::prost::alloc::string::String, - /// ISO calendar date (YYYY-MM-DD) of the accounting case. - #[prost(string, tag = "3")] - pub conversion_basis_date: ::prost::alloc::string::String, - /// Required. Profile whose rates apply. Consulted when the selection requests - /// a saved custom rate, and always required so the preview names its books. - #[prost(string, tag = "4")] - pub profile_name: ::prost::alloc::string::String, - /// Empty uses previous publication and ECB. - #[prost(message, optional, tag = "5")] - pub exchange_rate_selection: ::core::option::Option, -} -/// The conversion and immutable local ECB observation that would be used now. -/// No rounding is performed and no conversion evidence is persisted. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct PreviewEcbConversionResponse { - #[prost(string, tag = "1")] - pub original_amount: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub original_currency: ::prost::alloc::string::String, - #[prost(string, tag = "3")] - pub converted_amount: ::prost::alloc::string::String, - #[prost(string, tag = "4")] - pub conversion_basis_date: ::prost::alloc::string::String, - #[prost(string, tag = "5")] - pub determination_method: ::prost::alloc::string::String, - #[prost(string, tag = "6")] - pub rounding_method: ::prost::alloc::string::String, - #[prost(string, optional, tag = "7")] - pub rate_date: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, optional, tag = "8")] - pub units_per_pivot: ::core::option::Option<::prost::alloc::string::String>, - #[prost(int64, optional, tag = "9")] - pub rate_observation_id: ::core::option::Option, - #[prost(string, optional, tag = "10")] - pub observation_hash: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, optional, tag = "11")] - pub source_payload_hash: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, optional, tag = "12")] - pub rate_fetched_at: ::core::option::Option<::prost::alloc::string::String>, - #[prost(int64, optional, tag = "13")] - pub import_batch_id: ::core::option::Option, - #[prost(string, optional, tag = "14")] - pub source_endpoint: ::core::option::Option<::prost::alloc::string::String>, - /// Present only when determination_method is custom_rate. rate_date and - /// units_per_pivot then describe the hand-entered row identified here, and - /// the ECB observation fields above are empty. - #[prost(int64, optional, tag = "15")] - pub custom_rate_table_definition_id: ::core::option::Option, - #[prost(int64, optional, tag = "16")] - pub custom_rate_record_id: ::core::option::Option, - #[prost(int64, optional, tag = "17")] - pub custom_rate_row_revision: ::core::option::Option, - #[prost(string, optional, tag = "18")] - pub custom_rate_note: ::core::option::Option<::prost::alloc::string::String>, - /// Accountant-supplied justification for an exceptional publication date or - /// one-off manual rate. Empty for the normal/default rule. - #[prost(enumeration = "ExchangeRateDateRule", tag = "19")] - pub applied_date_rule: i32, - #[prost(enumeration = "ExchangeRateSource", tag = "20")] - pub applied_source: i32, - #[prost(string, tag = "21")] - pub selection_reason: ::prost::alloc::string::String, - /// The currency converted_amount is stated in: the profile's accounting - /// currency (EUR unless the profile keeps its books in something else). - #[prost(string, tag = "22")] - pub converted_currency: ::prost::alloc::string::String, - /// True when reaching converted_currency needed two hops (original currency - /// to EUR, then EUR to converted_currency) because neither is EUR. The - /// fields above then describe only the second, final hop; the full - /// provenance of both hops is available from ListEcbConversionEvidence - /// after posting. - #[prost(bool, tag = "23")] - pub via_two_hop_conversion: bool, -} -/// Identify one logical money cell whose conversion history should be inspected. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ListEcbConversionEvidenceRequest { - /// Required. Profile containing the table. - #[prost(string, tag = "1")] - pub profile_name: ::prost::alloc::string::String, - /// Required. Logical table name within the profile. - #[prost(string, tag = "2")] - pub table_name: ::prost::alloc::string::String, - /// Required. Dynamic table row id. - #[prost(int64, tag = "3")] - pub record_id: i64, - /// Required. Current display name of the money column. The server resolves it - /// to the stable physical column name used by immutable evidence. - #[prost(string, tag = "4")] - pub column_name: ::prost::alloc::string::String, - /// Optional. Maximum rows to return. Zero uses 100; maximum is 500. - #[prost(int32, tag = "5")] - pub limit: i32, - /// Optional cursor. Return evidence ids lower than this value. - #[prost(int64, optional, tag = "6")] - pub before_evidence_id: ::core::option::Option, -} -/// One immutable explanation of how a posted EUR value was determined. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct EcbConversionEvidence { - #[prost(int64, tag = "1")] - pub evidence_id: i64, - /// Exact committed version of the target dynamic-table row. - #[prost(int64, tag = "2")] - pub row_revision: i64, - #[prost(string, tag = "3")] - pub original_amount: ::prost::alloc::string::String, - #[prost(string, tag = "4")] - pub original_currency: ::prost::alloc::string::String, - #[prost(string, tag = "5")] - pub converted_amount: ::prost::alloc::string::String, - #[prost(string, tag = "6")] - pub conversion_context: ::prost::alloc::string::String, - #[prost(string, tag = "7")] - pub conversion_basis_date: ::prost::alloc::string::String, - #[prost(string, tag = "8")] - pub determination_method: ::prost::alloc::string::String, - #[prost(string, tag = "9")] - pub rounding_method: ::prost::alloc::string::String, - #[prost(string, optional, tag = "10")] - pub rate_date: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, optional, tag = "11")] - pub units_per_pivot: ::core::option::Option<::prost::alloc::string::String>, - #[prost(int64, optional, tag = "12")] - pub rate_observation_id: ::core::option::Option, - #[prost(string, optional, tag = "13")] - pub observation_hash: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, optional, tag = "14")] - pub source_payload_hash: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, optional, tag = "15")] - pub rate_fetched_at: ::core::option::Option<::prost::alloc::string::String>, - #[prost(int64, optional, tag = "16")] - pub import_batch_id: ::core::option::Option, - #[prost(string, optional, tag = "17")] - pub source_endpoint: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, tag = "18")] - pub evidence_created_at: ::prost::alloc::string::String, - /// Present only when determination_method is custom_rate. The hand-entered row - /// that supplied rate_date and units_per_pivot. The row itself stays editable, so - /// the figures above are the immutable record of what was actually applied. - #[prost(int64, optional, tag = "19")] - pub custom_rate_table_definition_id: ::core::option::Option, - #[prost(int64, optional, tag = "20")] - pub custom_rate_record_id: ::core::option::Option, - #[prost(int64, optional, tag = "21")] - pub custom_rate_row_revision: ::core::option::Option, - #[prost(string, optional, tag = "22")] - pub custom_rate_note: ::core::option::Option<::prost::alloc::string::String>, - /// Immutable audit data for an accountant-controlled exception. - #[prost(string, tag = "23")] - pub selection_reason: ::prost::alloc::string::String, - #[prost(string, optional, tag = "24")] - pub selected_by_user_id: ::core::option::Option<::prost::alloc::string::String>, - /// The currency converted_amount is stated in. Always EUR for the - /// original-currency-to-EUR leg; the profile's native currency for the - /// EUR-to-native leg that follows it when that native currency is not EUR. - #[prost(string, tag = "25")] - pub converted_currency: ::prost::alloc::string::String, - /// Set only on the EUR-to-native-currency leg of a two-hop conversion. Names - /// the original-currency-to-EUR leg's evidence_id, which produced this row's - /// original_amount/original_currency (always EUR). - #[prost(int64, optional, tag = "26")] - pub derived_from_evidence_id: ::core::option::Option, - /// The pivot currency of the rate source that priced this hop (EUR for - /// every hop today). A rate-based hop always has this currency on exactly - /// one side of original_currency/converted_currency. - #[prost(string, tag = "27")] - pub pivot_currency: ::prost::alloc::string::String, - /// Which rate source's policy governs this hop's profile ("ecb" for every - /// hop today). Absent only for already_same_currency, where no policy - /// needed to be consulted. This is about which policy applies, not about - /// who actually supplied this specific rate -- see observation_source_id - /// for that. - #[prost(string, optional, tag = "28")] - pub policy_source_id: ::core::option::Option<::prost::alloc::string::String>, - /// Which rate source actually supplied this hop's rate as its own official - /// observation. Present only when determination_method is - /// official_reference_rate; absent for custom_rate, manual_rate, and - /// already_same_currency, since none of those were priced by the policy's - /// source itself -- a human entered them. - #[prost(string, optional, tag = "29")] - pub observation_source_id: ::core::option::Option<::prost::alloc::string::String>, - /// Exact immutable profile policy version selected for this conversion's - /// accounting date. Absent only when no conversion policy was needed because - /// determination_method is already_same_currency. - #[prost(int64, optional, tag = "30")] - pub rate_source_policy_id: ::core::option::Option, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ListEcbConversionEvidenceResponse { - /// True when at least one evidence row for this cell used an ECB, saved - /// custom, or one-off manual rate. - #[prost(bool, tag = "1")] - pub has_exchange: bool, - /// Newest-first immutable history for this page. - #[prost(message, repeated, tag = "2")] - pub evidence: ::prost::alloc::vec::Vec, - /// True when another page exists using the last evidence_id as the cursor. - #[prost(bool, tag = "3")] - pub has_more: bool, -} -/// How the publication date is selected from the accounting basis date. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum ExchangeRateDateRule { - Unspecified = 0, - /// Latest TARGET publication strictly before the basis date. - PreviousPublication = 1, - /// Latest TARGET publication on or before the basis date. - OnOrBeforeDate = 2, - /// The exact publication date supplied in specific_rate_date. - SpecificPublicationDate = 3, -} -impl ExchangeRateDateRule { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "EXCHANGE_RATE_DATE_RULE_UNSPECIFIED", - Self::PreviousPublication => "EXCHANGE_RATE_DATE_RULE_PREVIOUS_PUBLICATION", - Self::OnOrBeforeDate => "EXCHANGE_RATE_DATE_RULE_ON_OR_BEFORE_DATE", - Self::SpecificPublicationDate => { - "EXCHANGE_RATE_DATE_RULE_SPECIFIC_PUBLICATION_DATE" - } - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "EXCHANGE_RATE_DATE_RULE_UNSPECIFIED" => Some(Self::Unspecified), - "EXCHANGE_RATE_DATE_RULE_PREVIOUS_PUBLICATION" => { - Some(Self::PreviousPublication) - } - "EXCHANGE_RATE_DATE_RULE_ON_OR_BEFORE_DATE" => Some(Self::OnOrBeforeDate), - "EXCHANGE_RATE_DATE_RULE_SPECIFIC_PUBLICATION_DATE" => { - Some(Self::SpecificPublicationDate) - } - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum ExchangeRateSource { - Unspecified = 0, - /// The rate source's own published reference rate (ECB's, today). - Official = 1, - /// A reusable rate from the profile's custom_exchange_rates table. - SavedCustom = 2, - /// A one-off rate recorded only in this posting's immutable evidence. - Manual = 3, -} -impl ExchangeRateSource { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "EXCHANGE_RATE_SOURCE_UNSPECIFIED", - Self::Official => "EXCHANGE_RATE_SOURCE_OFFICIAL", - Self::SavedCustom => "EXCHANGE_RATE_SOURCE_SAVED_CUSTOM", - Self::Manual => "EXCHANGE_RATE_SOURCE_MANUAL", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "EXCHANGE_RATE_SOURCE_UNSPECIFIED" => Some(Self::Unspecified), - "EXCHANGE_RATE_SOURCE_OFFICIAL" => Some(Self::Official), - "EXCHANGE_RATE_SOURCE_SAVED_CUSTOM" => Some(Self::SavedCustom), - "EXCHANGE_RATE_SOURCE_MANUAL" => Some(Self::Manual), - _ => None, - } - } + #[prost(message, repeated, tag = "11")] + pub currency_coverage: ::prost::alloc::vec::Vec, } /// Generated client implementations. pub mod ecb_service_client { @@ -402,11 +80,8 @@ pub mod ecb_service_client { )] use tonic::codegen::*; use tonic::codegen::http::Uri; - /// Read-only access to ECB conversion previews and audit evidence. - /// - /// This service exists for manual visual verification before and after posting. - /// Preview performs the authoritative unrounded backend calculation without - /// writing; evidence exposes conversions already recorded by posting workflows. + /// ECB-only operational details. Conversion selection and evidence live in the + /// provider-neutral exchange_rates contract. #[derive(Debug, Clone)] pub struct EcbServiceClient { inner: tonic::client::Grpc, @@ -487,67 +162,6 @@ pub mod ecb_service_client { self.inner = self.inner.max_encoding_message_size(limit); self } - /// Preview the exact conversion the backend would currently perform. - /// This reads local verified ECB data and never persists conversion evidence. - pub async fn preview_ecb_conversion( - &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.ecb.EcbService/PreviewEcbConversion", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new("komp_ac.ecb.EcbService", "PreviewEcbConversion"), - ); - self.inner.unary(req, path, codec).await - } - /// List immutable ECB conversion evidence for one money value. - pub async fn list_ecb_conversion_evidence( - &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.ecb.EcbService/ListEcbConversionEvidence", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new( - "komp_ac.ecb.EcbService", - "ListEcbConversionEvidence", - ), - ); - self.inner.unary(req, path, codec).await - } - /// Health of the reference-rate import pipeline: how far verified coverage - /// reaches, how far it should reach by now, and how the recent import - /// attempts went. Not profile-scoped -- one pipeline feeds every profile. pub async fn get_ecb_pipeline_status( &mut self, request: impl tonic::IntoRequest, @@ -589,26 +203,6 @@ pub mod ecb_service_server { /// Generated trait containing gRPC methods that should be implemented for use with EcbServiceServer. #[async_trait] pub trait EcbService: std::marker::Send + std::marker::Sync + 'static { - /// Preview the exact conversion the backend would currently perform. - /// This reads local verified ECB data and never persists conversion evidence. - async fn preview_ecb_conversion( - &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - /// List immutable ECB conversion evidence for one money value. - async fn list_ecb_conversion_evidence( - &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - /// Health of the reference-rate import pipeline: how far verified coverage - /// reaches, how far it should reach by now, and how the recent import - /// attempts went. Not profile-scoped -- one pipeline feeds every profile. async fn get_ecb_pipeline_status( &self, request: tonic::Request, @@ -617,11 +211,8 @@ pub mod ecb_service_server { tonic::Status, >; } - /// Read-only access to ECB conversion previews and audit evidence. - /// - /// This service exists for manual visual verification before and after posting. - /// Preview performs the authoritative unrounded backend calculation without - /// writing; evidence exposes conversions already recorded by posting workflows. + /// ECB-only operational details. Conversion selection and evidence live in the + /// provider-neutral exchange_rates contract. #[derive(Debug)] pub struct EcbServiceServer { inner: Arc, @@ -698,104 +289,6 @@ pub mod ecb_service_server { } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { - "/komp_ac.ecb.EcbService/PreviewEcbConversion" => { - #[allow(non_camel_case_types)] - struct PreviewEcbConversionSvc(pub Arc); - impl< - T: EcbService, - > tonic::server::UnaryService - for PreviewEcbConversionSvc { - type Response = super::PreviewEcbConversionResponse; - 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 { - ::preview_ecb_conversion(&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 = PreviewEcbConversionSvc(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.ecb.EcbService/ListEcbConversionEvidence" => { - #[allow(non_camel_case_types)] - struct ListEcbConversionEvidenceSvc(pub Arc); - impl< - T: EcbService, - > tonic::server::UnaryService< - super::ListEcbConversionEvidenceRequest, - > for ListEcbConversionEvidenceSvc { - type Response = super::ListEcbConversionEvidenceResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; - fn call( - &mut self, - request: tonic::Request< - super::ListEcbConversionEvidenceRequest, - >, - ) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { - ::list_ecb_conversion_evidence( - &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 = ListEcbConversionEvidenceSvc(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.ecb.EcbService/GetEcbPipelineStatus" => { #[allow(non_camel_case_types)] struct GetEcbPipelineStatusSvc(pub Arc); diff --git a/common/src/proto/komp_ac.exchange_rates.rs b/common/src/proto/komp_ac.exchange_rates.rs new file mode 100644 index 00000000..e4ff9888 --- /dev/null +++ b/common/src/proto/komp_ac.exchange_rates.rs @@ -0,0 +1,846 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExchangeRateSelection { + #[prost(enumeration = "ExchangeRateDateRule", tag = "1")] + pub date_rule: i32, + #[prost(enumeration = "ExchangeRateSource", tag = "2")] + pub source: i32, + #[prost(string, optional, tag = "3")] + pub specific_rate_date: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "4")] + pub manual_foreign_units: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "5")] + pub reason: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct PreviewDirectConversionRequest { + #[prost(string, tag = "1")] + pub original_amount: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub original_currency: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub conversion_basis_date: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub profile_name: ::prost::alloc::string::String, + #[prost(message, optional, tag = "5")] + pub exchange_rate_selection: ::core::option::Option, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct PreviewDirectConversionResponse { + #[prost(string, tag = "1")] + pub original_amount: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub original_currency: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub converted_amount: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub accounting_currency: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub conversion_basis_date: ::prost::alloc::string::String, + #[prost(string, tag = "6")] + pub determination_method: ::prost::alloc::string::String, + #[prost(string, tag = "7")] + pub rounding_method: ::prost::alloc::string::String, + #[prost(string, optional, tag = "8")] + pub rate_date: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "9")] + pub accounting_units: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "10")] + pub foreign_currency: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "11")] + pub foreign_units: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "12")] + pub source_id: ::prost::alloc::string::String, + #[prost(int64, optional, tag = "13")] + pub rate_observation_id: ::core::option::Option, + #[prost(string, optional, tag = "14")] + pub observation_hash: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "15")] + pub source_payload_hash: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "16")] + pub rate_fetched_at: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int64, optional, tag = "17")] + pub import_batch_id: ::core::option::Option, + #[prost(string, optional, tag = "18")] + pub source_endpoint: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int64, optional, tag = "19")] + pub custom_rate_table_definition_id: ::core::option::Option, + #[prost(int64, optional, tag = "20")] + pub custom_rate_record_id: ::core::option::Option, + #[prost(int64, optional, tag = "21")] + pub custom_rate_row_revision: ::core::option::Option, + #[prost(string, optional, tag = "22")] + pub custom_rate_note: ::core::option::Option<::prost::alloc::string::String>, + #[prost(enumeration = "ExchangeRateDateRule", tag = "23")] + pub applied_date_rule: i32, + #[prost(enumeration = "ExchangeRateSource", tag = "24")] + pub applied_source: i32, + #[prost(string, tag = "25")] + pub selection_reason: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ListConversionEvidenceRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, + #[prost(int64, tag = "3")] + pub record_id: i64, + #[prost(string, tag = "4")] + pub column_name: ::prost::alloc::string::String, + #[prost(int32, tag = "5")] + pub limit: i32, + #[prost(int64, optional, tag = "6")] + pub before_evidence_id: ::core::option::Option, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ConversionEvidence { + #[prost(int64, tag = "1")] + pub evidence_id: i64, + #[prost(int64, tag = "2")] + pub row_revision: i64, + #[prost(string, tag = "3")] + pub original_amount: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub original_currency: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub converted_amount: ::prost::alloc::string::String, + #[prost(string, tag = "6")] + pub accounting_currency: ::prost::alloc::string::String, + #[prost(string, tag = "7")] + pub accounting_units: ::prost::alloc::string::String, + #[prost(string, tag = "8")] + pub foreign_currency: ::prost::alloc::string::String, + #[prost(string, tag = "9")] + pub foreign_units: ::prost::alloc::string::String, + #[prost(string, tag = "10")] + pub conversion_context: ::prost::alloc::string::String, + #[prost(string, tag = "11")] + pub conversion_basis_date: ::prost::alloc::string::String, + #[prost(string, tag = "12")] + pub determination_method: ::prost::alloc::string::String, + #[prost(string, tag = "13")] + pub rounding_method: ::prost::alloc::string::String, + #[prost(string, optional, tag = "14")] + pub rate_date: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "15")] + pub source_id: ::prost::alloc::string::String, + #[prost(int64, optional, tag = "16")] + pub rate_observation_id: ::core::option::Option, + #[prost(string, optional, tag = "17")] + pub observation_hash: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "18")] + pub source_payload_hash: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "19")] + pub rate_fetched_at: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int64, optional, tag = "20")] + pub import_batch_id: ::core::option::Option, + #[prost(string, optional, tag = "21")] + pub source_endpoint: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "22")] + pub evidence_created_at: ::prost::alloc::string::String, + #[prost(int64, optional, tag = "23")] + pub custom_rate_table_definition_id: ::core::option::Option, + #[prost(int64, optional, tag = "24")] + pub custom_rate_record_id: ::core::option::Option, + #[prost(int64, optional, tag = "25")] + pub custom_rate_row_revision: ::core::option::Option, + #[prost(string, optional, tag = "26")] + pub custom_rate_note: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "27")] + pub selection_reason: ::prost::alloc::string::String, + #[prost(string, optional, tag = "28")] + pub selected_by_user_id: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListConversionEvidenceResponse { + #[prost(bool, tag = "1")] + pub has_exchange: bool, + #[prost(message, repeated, tag = "2")] + pub evidence: ::prost::alloc::vec::Vec, + #[prost(bool, tag = "3")] + pub has_more: bool, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GetProfileExchangeRateSettingsRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ProfileForeignCurrency { + #[prost(string, tag = "1")] + pub currency: ::prost::alloc::string::String, + #[prost(bool, tag = "2")] + pub coverage_complete: bool, + #[prost(string, optional, tag = "3")] + pub verified_from_date: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "4")] + pub verified_through_date: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProfileExchangeRateSettings { + #[prost(string, tag = "1")] + 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(message, repeated, tag = "4")] + pub foreign_currencies: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct AddProfileForeignCurrencyRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub currency: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ExchangeRateDateRule { + Unspecified = 0, + PreviousPublication = 1, + OnOrBeforeDate = 2, + SpecificPublicationDate = 3, +} +impl ExchangeRateDateRule { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "EXCHANGE_RATE_DATE_RULE_UNSPECIFIED", + Self::PreviousPublication => "EXCHANGE_RATE_DATE_RULE_PREVIOUS_PUBLICATION", + Self::OnOrBeforeDate => "EXCHANGE_RATE_DATE_RULE_ON_OR_BEFORE_DATE", + Self::SpecificPublicationDate => { + "EXCHANGE_RATE_DATE_RULE_SPECIFIC_PUBLICATION_DATE" + } + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "EXCHANGE_RATE_DATE_RULE_UNSPECIFIED" => Some(Self::Unspecified), + "EXCHANGE_RATE_DATE_RULE_PREVIOUS_PUBLICATION" => { + Some(Self::PreviousPublication) + } + "EXCHANGE_RATE_DATE_RULE_ON_OR_BEFORE_DATE" => Some(Self::OnOrBeforeDate), + "EXCHANGE_RATE_DATE_RULE_SPECIFIC_PUBLICATION_DATE" => { + Some(Self::SpecificPublicationDate) + } + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ExchangeRateSource { + Unspecified = 0, + Official = 1, + SavedCustom = 2, + Manual = 3, +} +impl ExchangeRateSource { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "EXCHANGE_RATE_SOURCE_UNSPECIFIED", + Self::Official => "EXCHANGE_RATE_SOURCE_OFFICIAL", + Self::SavedCustom => "EXCHANGE_RATE_SOURCE_SAVED_CUSTOM", + Self::Manual => "EXCHANGE_RATE_SOURCE_MANUAL", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "EXCHANGE_RATE_SOURCE_UNSPECIFIED" => Some(Self::Unspecified), + "EXCHANGE_RATE_SOURCE_OFFICIAL" => Some(Self::Official), + "EXCHANGE_RATE_SOURCE_SAVED_CUSTOM" => Some(Self::SavedCustom), + "EXCHANGE_RATE_SOURCE_MANUAL" => Some(Self::Manual), + _ => None, + } + } +} +/// Generated client implementations. +pub mod exchange_rate_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct ExchangeRateServiceClient { + inner: tonic::client::Grpc, + } + impl ExchangeRateServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ExchangeRateServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ExchangeRateServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ExchangeRateServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn preview_direct_conversion( + &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/PreviewDirectConversion", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.exchange_rates.ExchangeRateService", + "PreviewDirectConversion", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn list_conversion_evidence( + &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/ListConversionEvidence", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.exchange_rates.ExchangeRateService", + "ListConversionEvidence", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_profile_exchange_rate_settings( + &mut self, + request: impl tonic::IntoRequest< + super::GetProfileExchangeRateSettingsRequest, + >, + ) -> 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/GetProfileExchangeRateSettings", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.exchange_rates.ExchangeRateService", + "GetProfileExchangeRateSettings", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn add_profile_foreign_currency( + &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/AddProfileForeignCurrency", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.exchange_rates.ExchangeRateService", + "AddProfileForeignCurrency", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod exchange_rate_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// 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 { + async fn preview_direct_conversion( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn list_conversion_evidence( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_profile_exchange_rate_settings( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn add_profile_foreign_currency( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + #[derive(Debug)] + pub struct ExchangeRateServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl ExchangeRateServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for ExchangeRateServiceServer + where + T: ExchangeRateService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/komp_ac.exchange_rates.ExchangeRateService/PreviewDirectConversion" => { + #[allow(non_camel_case_types)] + struct PreviewDirectConversionSvc( + pub Arc, + ); + impl< + T: ExchangeRateService, + > tonic::server::UnaryService + for PreviewDirectConversionSvc { + type Response = super::PreviewDirectConversionResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::PreviewDirectConversionRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::preview_direct_conversion( + &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 = PreviewDirectConversionSvc(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/ListConversionEvidence" => { + #[allow(non_camel_case_types)] + struct ListConversionEvidenceSvc(pub Arc); + impl< + T: ExchangeRateService, + > tonic::server::UnaryService + for ListConversionEvidenceSvc { + type Response = super::ListConversionEvidenceResponse; + 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_conversion_evidence( + &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 = ListConversionEvidenceSvc(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/GetProfileExchangeRateSettings" => { + #[allow(non_camel_case_types)] + struct GetProfileExchangeRateSettingsSvc( + pub Arc, + ); + impl< + T: ExchangeRateService, + > tonic::server::UnaryService< + super::GetProfileExchangeRateSettingsRequest, + > for GetProfileExchangeRateSettingsSvc { + type Response = super::ProfileExchangeRateSettings; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::GetProfileExchangeRateSettingsRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_profile_exchange_rate_settings( + &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 = GetProfileExchangeRateSettingsSvc(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/AddProfileForeignCurrency" => { + #[allow(non_camel_case_types)] + struct AddProfileForeignCurrencySvc( + pub Arc, + ); + impl< + T: ExchangeRateService, + > tonic::server::UnaryService< + super::AddProfileForeignCurrencyRequest, + > for AddProfileForeignCurrencySvc { + type Response = super::ProfileForeignCurrency; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::AddProfileForeignCurrencyRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::add_profile_foreign_currency( + &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 = AddProfileForeignCurrencySvc(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) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for ExchangeRateServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "komp_ac.exchange_rates.ExchangeRateService"; + impl tonic::server::NamedService for ExchangeRateServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/common/src/proto/komp_ac.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index 6773edab..0a96ce46 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -51,6 +51,13 @@ 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. + /// Currently only "ecb" is available, and it requires EUR accounting. + #[prost(string, tag = "11")] + pub rate_source_id: ::prost::alloc::string::String, + /// Append-only foreign currencies initially enabled for this profile. + #[prost(string, repeated, tag = "12")] + pub foreign_currencies: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// When true, the table is stored once in the global physical schema and is /// visible from every profile. profile_name and accounting_currency are ignored. #[prost(bool, tag = "9")] diff --git a/common/src/proto/komp_ac.tables_data.rs b/common/src/proto/komp_ac.tables_data.rs index bedbe498..d259b25c 100644 --- a/common/src/proto/komp_ac.tables_data.rs +++ b/common/src/proto/komp_ac.tables_data.rs @@ -55,7 +55,7 @@ pub struct PostAccountingTableDataRequest { pub row: ::core::option::Option, #[prost(message, optional, tag = "2")] pub exchange_rate_selection: ::core::option::Option< - super::ecb::ExchangeRateSelection, + super::exchange_rates::ExchangeRateSelection, >, } /// Insert response. @@ -173,7 +173,7 @@ pub struct PutAccountingTableDataRequest { pub update: ::core::option::Option, #[prost(message, optional, tag = "2")] pub exchange_rate_selection: ::core::option::Option< - super::ecb::ExchangeRateSelection, + super::exchange_rates::ExchangeRateSelection, >, #[prost(bool, tag = "3")] pub confirmed: bool, diff --git a/server b/server index b864da0a..b929f654 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit b864da0a9b76062bb0f6236e9b6e3d0e82bc11fe +Subproject commit b929f654ed1f4443967fd959ee8e6b5213d45c10 diff --git a/web/src/lib.rs b/web/src/lib.rs index 9f6403f7..f51dbe74 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -7,21 +7,21 @@ use axum::{ routing::get, }; +mod authz; +mod i18n; mod pages; mod schema; mod services; mod ui; -mod authz; -mod i18n; // The server's system column vocabulary, read out of `common` the same way the // generated protos are: this crate compiles that source tree directly instead // of depending on the crate. Only `is_system_column` is used here. -#[path = "../../common/src/system_column.rs"] -#[allow(dead_code)] -mod system_column; #[path = "../../common/src/grpc_error.rs"] #[allow(dead_code)] mod grpc_error; +#[path = "../../common/src/system_column.rs"] +#[allow(dead_code)] +mod system_column; mod analytics { include!(concat!( env!("CARGO_MANIFEST_DIR"), @@ -78,12 +78,17 @@ mod definitions { "/../common/src/proto/komp_ac.ecb.rs" )); } + pub mod exchange_rates { + include!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../common/src/proto/komp_ac.exchange_rates.rs" + )); + } } use auth::auth_service_client::AuthServiceClient; use definitions::{ - ecb, - table_definition::table_definition_client::TableDefinitionClient, + ecb, 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, diff --git a/web/src/pages/add_table/draft.rs b/web/src/pages/add_table/draft.rs index ed1ed4d5..e726d400 100644 --- a/web/src/pages/add_table/draft.rs +++ b/web/src/pages/add_table/draft.rs @@ -13,10 +13,8 @@ use crate::{ definitions::table_definition::{GeneratedColumnAlias, PostTableDefinitionRequest}, + schema::{ColumnCatalog, ColumnDraft, proto_columns, validate_identifier, validate_table_name}, {i18n::Locale, tr}, - schema::{ - ColumnCatalog, ColumnDraft, proto_columns, validate_identifier, validate_table_name, - }, }; /// The compound type that also brings a system column with it. Which columns @@ -68,6 +66,8 @@ pub(crate) struct TableDraft { pub creating_new_profile: bool, pub global: bool, pub accounting_currency: String, + pub rate_source_id: String, + pub foreign_currencies: String, pub table_name: String, @@ -98,6 +98,7 @@ impl TableDraft { pub(crate) fn new() -> Self { Self { accounting_currency: "EUR".to_string(), + rate_source_id: "ecb".to_string(), columns: ColumnDraft::new(ColumnCatalog::default()), ..Self::default() } @@ -116,11 +117,7 @@ impl TableDraft { // ---- mutations ------------------------------------------------------- /// Removes one column, and drops it from the display columns with it. - pub(crate) fn remove_column( - &mut self, - locale: Locale, - index: usize, - ) -> Result { + pub(crate) fn remove_column(&mut self, locale: Locale, index: usize) -> Result { let removed = self.columns.remove(locale, index)?; self.row_display_columns .retain(|display| display != &removed.name); @@ -364,9 +361,7 @@ impl TableDraft { ); for (source, alias) in &renames { - if let Some(error) = - validate_identifier(locale, alias, "label-column-alias", true) - { + if let Some(error) = validate_identifier(locale, alias, "label-column-alias", true) { return Err(error); } if taken.iter().filter(|name| *name == alias).count() > 1 { @@ -574,6 +569,21 @@ impl TableDraft { } else { String::new() }, + rate_source_id: if self.creating_new_profile && !self.global { + self.rate_source_id.trim().to_ascii_lowercase() + } else { + String::new() + }, + foreign_currencies: if self.creating_new_profile && !self.global { + self.foreign_currencies + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_ascii_uppercase) + .collect() + } else { + Vec::new() + }, row_display_columns: self.row_display_columns.clone(), global: self.global, // The generated columns are named in the same request that creates @@ -634,7 +644,13 @@ mod tests { draft.accounting_currency = "eur".to_string(); assert!(draft.validate(crate::i18n::Locale::default()).is_ok()); - assert_eq!(draft.into_request(crate::i18n::Locale::default()).unwrap().accounting_currency, "EUR"); + assert_eq!( + draft + .into_request(crate::i18n::Locale::default()) + .unwrap() + .accounting_currency, + "EUR" + ); } /// Every column an ACCOUNTING row generates may be aliased, including the @@ -740,16 +756,28 @@ mod tests { source: "debit".to_string(), alias: "Md".to_string(), }]; - assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "an alias is a column name"); + assert!( + draft.validate(crate::i18n::Locale::default()).is_err(), + "an alias is a column name" + ); draft.generated_aliases[0].alias = "note".to_string(); - assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "a declared column holds the name"); + assert!( + draft.validate(crate::i18n::Locale::default()).is_err(), + "a declared column holds the name" + ); draft.generated_aliases[0].alias = "credit".to_string(); - assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "another generated column does"); + assert!( + draft.validate(crate::i18n::Locale::default()).is_err(), + "another generated column does" + ); draft.generated_aliases[0].alias = "id".to_string(); - assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "a system column does"); + assert!( + draft.validate(crate::i18n::Locale::default()).is_err(), + "a system column does" + ); draft.generated_aliases[0].alias = "md".to_string(); assert!(draft.validate(crate::i18n::Locale::default()).is_ok()); @@ -791,7 +819,9 @@ mod tests { #[test] fn a_table_with_no_columns_is_refused() { let mut draft = draft_with_column("total", "int"); - draft.remove_column(crate::i18n::Locale::default(), 0).unwrap(); + draft + .remove_column(crate::i18n::Locale::default(), 0) + .unwrap(); assert!(draft.validate(crate::i18n::Locale::default()).is_err()); } @@ -850,9 +880,20 @@ mod tests { // never the definition row itself. assert_eq!( draft.row_display_column_names(), - ["number", "name", "tax_point_date", "debit", "credit", "account"] + [ + "number", + "name", + "tax_point_date", + "debit", + "credit", + "account" + ] + ); + assert!( + !draft + .row_display_column_names() + .contains(&"accounting".to_string()) ); - assert!(!draft.row_display_column_names().contains(&"accounting".to_string())); // `name` is the second candidate, and choosing it names the generated // column rather than the row it came from. @@ -879,7 +920,9 @@ mod tests { draft.toggle_row_display_candidate(1); assert_eq!(draft.row_display_columns, vec!["number"]); - draft.remove_column(crate::i18n::Locale::default(), 0).unwrap(); + draft + .remove_column(crate::i18n::Locale::default(), 0) + .unwrap(); assert!(draft.row_display_columns.is_empty()); } @@ -919,7 +962,9 @@ mod tests { draft.toggle_row_display_candidate(1); // number draft.toggle_row_display_candidate(2); // issued_on - draft.move_column(crate::i18n::Locale::default(), 0, 1).unwrap(); + draft + .move_column(crate::i18n::Locale::default(), 0, 1) + .unwrap(); assert_eq!( draft @@ -993,7 +1038,13 @@ mod tests { let mut draft = draft_with_column("number", "text"); draft.columns.toggle_indexed(0); - assert_eq!(draft.into_request(crate::i18n::Locale::default()).unwrap().indexes, vec!["number"]); + assert_eq!( + draft + .into_request(crate::i18n::Locale::default()) + .unwrap() + .indexes, + vec!["number"] + ); } /// The request the builder sends is one the server will take: a link @@ -1090,6 +1141,10 @@ mod tests { source: "name".to_string(), alias: "row_revision".to_string(), }]; - assert!(draft.validate_generated_aliases(crate::i18n::Locale::default()).is_err()); + assert!( + draft + .validate_generated_aliases(crate::i18n::Locale::default()) + .is_err() + ); } } diff --git a/web/src/pages/add_table/state.rs b/web/src/pages/add_table/state.rs index acfee64e..8870f363 100644 --- a/web/src/pages/add_table/state.rs +++ b/web/src/pages/add_table/state.rs @@ -37,6 +37,10 @@ pub(crate) struct BuilderForm { #[serde(default)] pub accounting_currency: String, #[serde(default)] + pub rate_source_id: String, + #[serde(default)] + pub foreign_currencies: String, + #[serde(default)] pub global: bool, #[serde(default)] pub table_name: String, @@ -163,6 +167,8 @@ impl BuilderForm { profile_name_input: self.profile_name_input.clone(), creating_new_profile, accounting_currency: self.accounting_currency.clone(), + rate_source_id: self.rate_source_id.clone(), + foreign_currencies: self.foreign_currencies.clone(), global: self.global, table_name: self.table_name.clone(), columns, @@ -233,7 +239,11 @@ impl AddTablePageState { tags.push("quantity ledger".to_string()); } if !column.currency.is_empty() { - tags.push(format!("{}, {}", column.currency, column.money_mode.label())); + tags.push(format!( + "{}, {}", + column.currency, + column.money_mode.label() + )); } rows.push(ColumnRow { index: Some(index), @@ -266,7 +276,11 @@ impl AddTablePageState { for generated in generated_columns { let mut tags = vec![format!("generated by {}", column.data_type)]; if generated.inherits_currency { - tags.push(format!("{}, {}", column.currency, column.money_mode.label())); + tags.push(format!( + "{}, {}", + column.currency, + column.money_mode.label() + )); } rows.push(ColumnRow { index: None, @@ -502,15 +516,18 @@ mod tests { error: None, }; page.draft.columns.catalog = crate::schema::tests::catalog(); - page.draft.columns.added.push(crate::schema::ColumnDefinition { - name: "accounting".to_string(), - data_type: "accounting".to_string(), - indexed: false, - quantity_ledger: false, - required: false, - money_mode: MoneyMode::Exact, - currency: "EUR".to_string(), - }); + page.draft + .columns + .added + .push(crate::schema::ColumnDefinition { + name: "accounting".to_string(), + data_type: "accounting".to_string(), + indexed: false, + quantity_ledger: false, + required: false, + money_mode: MoneyMode::Exact, + currency: "EUR".to_string(), + }); let rows = page.column_rows(); assert_eq!( @@ -537,9 +554,16 @@ mod tests { ); assert!(row("number").first); assert!(row("accounting").last); - assert!(!row("accounting").indexable, "a definition row is no column"); + assert!( + !row("accounting").indexable, + "a definition row is no column" + ); assert!(row("debit").tags.contains(&"EUR, exact".to_string())); - assert!(row("name").tags.contains(&"generated by accounting".to_string())); + assert!( + row("name") + .tags + .contains(&"generated by accounting".to_string()) + ); } /// The generated columns of an ACCOUNTING row are the user's to alias, and @@ -586,7 +610,11 @@ mod tests { assert_eq!(row("account").alias_source.as_deref(), Some("account")); assert_eq!(row("account").alias, "ucet"); assert!(row("account_id").alias_source.is_none()); - assert!(row("account_id").tags.contains(&"written as ucet".to_string())); + assert!( + row("account_id") + .tags + .contains(&"written as ucet".to_string()) + ); assert!( row("accounting").alias_source.is_none(), "a declared column is named where it is described" diff --git a/web/templates/pages/add_table/builder.html b/web/templates/pages/add_table/builder.html index a5924b3e..e76ed268 100644 --- a/web/templates/pages/add_table/builder.html +++ b/web/templates/pages/add_table/builder.html @@ -88,8 +88,19 @@ #} {{ nav.tr("builder-accounting-currency-hint") }} + + {% else %} + + {% endif %} {#