From dd8f94c9ee913e496edb5db4412ba90f84b1cabe Mon Sep 17 00:00:00 2001 From: Priec Date: Sat, 18 Jul 2026 15:50:11 +0200 Subject: [PATCH] endpoint for preview ecb, instead of only commited one --- common/proto/ecb.proto | 52 ++++++++- common/src/proto/descriptor.bin | Bin 110077 -> 113663 bytes common/src/proto/komp_ac.ecb.rs | 184 ++++++++++++++++++++++++++++++-- server | 2 +- 4 files changed, 228 insertions(+), 10 deletions(-) diff --git a/common/proto/ecb.proto b/common/proto/ecb.proto index efea97d..007e49f 100644 --- a/common/proto/ecb.proto +++ b/common/proto/ecb.proto @@ -2,16 +2,62 @@ syntax = "proto3"; package komp_ac.ecb; -// Read-only access to ECB conversion audit evidence. +// Read-only access to ECB conversion previews and audit evidence. // -// This service exists for manual visual verification. It exposes conversions -// already recorded by backend posting workflows and never recalculates money. +// 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. 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); } +// Accounting date rule used to select an ECB publication. +enum EcbConversionContext { + ECB_CONVERSION_CONTEXT_UNSPECIFIED = 0; + ECB_CONVERSION_CONTEXT_ORDINARY_TRANSACTION = 1; + ECB_CONVERSION_CONTEXT_FINANCIAL_STATEMENT = 2; + ECB_CONVERSION_CONTEXT_DECISIVE_DATE = 3; +} + +// 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; + EcbConversionContext conversion_context = 3; + + // ISO calendar date (YYYY-MM-DD). Its meaning is selected by + // conversion_context: transaction, statement, or decisive date. + string anchor_date = 4; +} + +// 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 eur_amount = 3; + EcbConversionContext conversion_context = 4; + string anchor_date = 5; + string determination_method = 6; + string rounding_method = 7; + optional string rate_date = 8; + optional string units_per_eur = 9; + optional int64 rate_observation_id = 10; + optional string observation_hash = 11; + optional string source_payload_hash = 12; + optional string rate_fetched_at = 13; + optional int64 import_batch_id = 14; + optional string source_endpoint = 15; +} + // Identify one logical money cell whose conversion history should be inspected. message ListEcbConversionEvidenceRequest { // Required. Profile containing the table. diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 13e1018a85c76729f9c66ee9754cf224401153ce..13766f4e3fe1fb6fac9e25bf6b2c70802b85d341 100644 GIT binary patch delta 4781 zcma)9O>A3P752N&cI=x!=S`c$PSV``Cr;x0w9U^9GbK&j%Ct>N;-v-L(1;})Ho%f45@Lba0t+;35Pavo>q{%O5?xgJa?khO zbIv{Iv-9si#{ctq{O5m8@BQw5p0urU?xgN|uF?6-Jw7R!Rt~=!|EtD_Zr1JRj(wW9 z%G+-3xm|BKZf)QGYSV6b#tiRu>&_FWW>!n)vD>V9gPOlLlua|C%_jF#?ZfS6y>8d6 zm&Q&17ei-fb)5FBj^$ptD)*W`Zu0CGp8xxyiL22-ikmg-saxMMJ$wA`aX$R!tQse7 ztzo}4EsgnK8=5lc`s9AnZq~PgoMl#Y(f^y37W4l$G^fS=^zfqA;a?pd*Ao6$hNrZo z|DEBS#{FLpuV|hAKZaMeE`M@lS?l(1jVx$A{)hO#*Z0b_J zredw^Z#91AO?@z?`FMW&a%p>S_b|VIP}tk0AB*`r#nP?agPZy7!j(dPClxW4`21Vq z_V#xQyIcErOU3=I-Gi;|B1BS}k>goKXuYm{1@XJvg{|wQgW^^(ew_*dPA7C zik}hrmMGhg-MUR!n2$ZXE>7Hr=hU9$xR9BO6T6O>#gaHl=Bcdb5gWY9MpZ6%D&;qaS81DC4AA zt~yrmRAEzNl;qi1ns@SOjC$JA``H{_M+j&AY_g4zO8?BPjKt9ZTa`zY^8j0HBLogC zugE}vLAD}~7y<{`LL1T9(y|OhZD}^&l4m+1(`>qp5SgCK@=r9*qNw~_WLnF7tciT^ z*iP-FiMPPS3iH($k)xjzJGSK<)03K2SiCSX{SB{sycSf6SY?GzEzr3 z0lwwXM*~7I^iQ6OGCmdRF9oI&Jw(4xT&L!Ros3eq&Lt+OnkaQrA7vwyK7kmaL>nP+ zG&$HtsC#s1Skkl>oSX%(51s~|P(13gNCxvnXW)sEbCQStjIo@QWN||MZ3NobK=7uZ zG0?^q7Wp@^M1Vk3ES>qfC>-q3BHOK6#jJ;OG1CS!KLf%9?@fKcN6P%E4SjW%UL4gvsp3yMbN-4=z9b(IfuJqn0Ye%Bm)W?a#R3$*yVhbc@RpO|>S!zm-m(~zJmj^ani&UgMO_yM zZACRR4%&*`Dvfm*`YIFhx(?*L$^w%{0UZ!o?d)rx(f+L(Lv09^)pMir@`RzE58V*F z1XRu|l>}7IE0qLNId6nZj7W%_9}j&&V@X55z_L<536Tp*BngoVt#>A9V=`r+8l}ZmpHkC*ZL^f3_JrLPcsq{c( z6RF&XwP0hS??-O>2V=MUX1Diqge&YRfHY4*;Ou zlEnkjnY0HW&~8b4(8)7fhtg|-z&lht(ma%2i!<;JrPt~NZmTS**S8go%DF9{6@e6~ zvP7%yNH@}hZr@>v;A>A55WG9d(0_oyyCeNar@Zb;uLTHDXqrHv-IY%Z1lnEcwK{zn zxF@|90JM85OY+Qnfz`tHaR}Z$>9snU=6%&nvdR03*8$#r)yxji?yF|PB_F8j$R!`h zvCtX0a<0zl-E5+Rp-)KV&BlaHj=A`~i*q}S>UHfc(O1AvODRLCVwB| delta 1893 zcmX|?%WvaU6o=zu$FZ;Tywgtln5ItKyc6fq^rfOe5wBqch(W*tA(W;DRqZ5IT4tz_ zh{Ui!f}JHmEN2T6NGxUrYc{ZDF`HTOFR()JyUz9P$|v`H-#zE}`u4wn%zvMlKR;bw z|Gmg;e{}3!Oea?p?wn2TU3@ScdH8ALb7F3gD!IRWS}4TRy)QM!UK}02^$75rLyVdlBFuU!9=i^-*b>5eAj`~LLu zbTYnV?(lr-4^OV$sXv-bPy7@2_}V=lj?Vn?iHr2B)A2{{gUR&l!}H05OLvH0$3AG0 zKRh3Od_KH_@!n+YUwaIHx*5~BZdsh&VTKta)WXOKzN@_Ki3Mh^vm}d)1w!OL7ezYP zolJ+l$H)MZiERXLx8LOg$yimk%M3%f^jz>;yVt>E9M5D4S}Y(fXD>z(T)CxX_G}1Y zX(4bshu{A6igt75hRo5Bvv4!dyf6o)JFZiMN+Dt=I$y#F$%$gW! znU+EV0c>Si5F#X&t@*$Z!B)n=<`|YLv?ehPSVcCkk?>dSNGaxAS-mAO%z0L&@C0CQ zG3u(~#Hg$GigXgV>RLf^2x_WIG3%Pb#H?#}RHc}8O;w3e)yb7cG3`28vnT{tw^ySG zT)nU^w?k%wmgSNdaYJnv2iB0UR--tu243xaC??mNv>`DQSW{spu%;~rHjATk6I|0R zN-n|mmKtUPTuX5Ya4j{=1hAGGrp0wnb!VYhPaX@6EJ!?Oerypz;&IvPj4W6@)vL|* zHkD-2HY{yrv0-T|iw#SgNAJ#tq`k2vJ78SzP+dwGBpoGTkaXk;)re^`gt@IK6_!q= zCM^!vyHu4+4lG?|abW4HQXG`h<, + #[prost(string, optional, tag = "9")] + pub units_per_eur: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int64, optional, tag = "10")] + pub rate_observation_id: ::core::option::Option, + #[prost(string, optional, tag = "11")] + pub observation_hash: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "12")] + pub source_payload_hash: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "13")] + pub rate_fetched_at: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int64, optional, tag = "14")] + pub import_batch_id: ::core::option::Option, + #[prost(string, optional, tag = "15")] + pub source_endpoint: ::core::option::Option<::prost::alloc::string::String>, +} /// Identify one logical money cell whose conversion history should be inspected. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListEcbConversionEvidenceRequest { @@ -72,6 +122,43 @@ pub struct ListEcbConversionEvidenceResponse { #[prost(bool, tag = "3")] pub has_more: bool, } +/// Accounting date rule used to select an ECB publication. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum EcbConversionContext { + Unspecified = 0, + OrdinaryTransaction = 1, + FinancialStatement = 2, + DecisiveDate = 3, +} +impl EcbConversionContext { + /// 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 => "ECB_CONVERSION_CONTEXT_UNSPECIFIED", + Self::OrdinaryTransaction => "ECB_CONVERSION_CONTEXT_ORDINARY_TRANSACTION", + Self::FinancialStatement => "ECB_CONVERSION_CONTEXT_FINANCIAL_STATEMENT", + Self::DecisiveDate => "ECB_CONVERSION_CONTEXT_DECISIVE_DATE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "ECB_CONVERSION_CONTEXT_UNSPECIFIED" => Some(Self::Unspecified), + "ECB_CONVERSION_CONTEXT_ORDINARY_TRANSACTION" => { + Some(Self::OrdinaryTransaction) + } + "ECB_CONVERSION_CONTEXT_FINANCIAL_STATEMENT" => { + Some(Self::FinancialStatement) + } + "ECB_CONVERSION_CONTEXT_DECISIVE_DATE" => Some(Self::DecisiveDate), + _ => None, + } + } +} /// Generated client implementations. pub mod ecb_service_client { #![allow( @@ -83,10 +170,11 @@ pub mod ecb_service_client { )] use tonic::codegen::*; use tonic::codegen::http::Uri; - /// Read-only access to ECB conversion audit evidence. + /// Read-only access to ECB conversion previews and audit evidence. /// - /// This service exists for manual visual verification. It exposes conversions - /// already recorded by backend posting workflows and never recalculates money. + /// 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. #[derive(Debug, Clone)] pub struct EcbServiceClient { inner: tonic::client::Grpc, @@ -167,6 +255,34 @@ 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, @@ -212,6 +328,15 @@ 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, @@ -221,10 +346,11 @@ pub mod ecb_service_server { tonic::Status, >; } - /// Read-only access to ECB conversion audit evidence. + /// Read-only access to ECB conversion previews and audit evidence. /// - /// This service exists for manual visual verification. It exposes conversions - /// already recorded by backend posting workflows and never recalculates money. + /// 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. #[derive(Debug)] pub struct EcbServiceServer { inner: Arc, @@ -301,6 +427,52 @@ 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); diff --git a/server b/server index fd73fe9..b0c7082 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit fd73fe9b2a1bb449addafa20e5c116127939b99f +Subproject commit b0c708244bec8d9673e1159e1051ae21447b860a