conversions web + put table definition

This commit is contained in:
Filipriec
2026-08-24 09:35:19 +02:00
parent 2e95201a71
commit 5d6f53c96e
25 changed files with 1542 additions and 96 deletions

View File

@@ -0,0 +1,250 @@
//! HTTP handlers for the provider-neutral exchange-rate administration workspace.
use axum::{
extract::{Query, State},
http::{HeaderMap, StatusCode},
response::{Html, IntoResponse, Redirect, Response},
};
use axum_extra::extract::Form;
use crate::{
AppState,
definitions::exchange_rates::{
AddProfileCurrencySourceRequest, ExchangeRateDateRule, ExchangeRateSource,
ListConversionEvidenceRequest,
},
services::{authenticated_request, reject_cross_site},
};
use super::{
state::LoadError,
workspace_loader::load_workspace,
workspace_state::{
ConfigurationForm, EvidenceForm, EvidenceView, PreviewForm, PreviewView, WorkspaceInputs,
WorkspaceQuery,
},
workspace_ui,
};
pub(crate) async fn legacy_ecb() -> Redirect {
Redirect::permanent("/admin/exchange-rates#providers")
}
pub(crate) async fn page(
State(state): State<AppState>,
headers: HeaderMap,
Query(query): Query<WorkspaceQuery>,
) -> Response {
render_loaded(
&headers,
load_workspace(state, &headers, WorkspaceInputs::for_profile(query.profile)).await,
StatusCode::OK,
)
}
pub(crate) async fn add_configuration(
State(state): State<AppState>,
headers: HeaderMap,
Form(form): Form<ConfigurationForm>,
) -> Response {
if let Some(rejection) = reject_cross_site(&headers) {
return rejection;
}
let mut inputs = WorkspaceInputs::for_profile(form.profile.clone());
inputs.configuration = form.clone();
let added_source = form.source_id.trim().to_string();
let request = AddProfileCurrencySourceRequest {
profile_name: form.profile,
currency: form.currency.trim().to_ascii_uppercase(),
source_id: form.source_id.trim().to_string(),
make_default: form.make_default,
};
let Ok(request) = authenticated_request(&headers, request) else {
return Redirect::to("/login").into_response();
};
let mut exchange_rates = state.exchange_rates.clone();
match exchange_rates.add_profile_currency_source(request).await {
Ok(currency) => {
let currency = currency.into_inner();
inputs.configuration = ConfigurationForm::default();
inputs.status = Some(crate::tr!(
crate::i18n::Locale::from_headers(&headers),
"rates-configuration-added",
"currency" => currency.currency,
"source" => added_source,
));
render_loaded(
&headers,
load_workspace(state, &headers, inputs).await,
StatusCode::OK,
)
}
Err(error) => {
inputs.error = Some(error.message().to_string());
render_loaded(
&headers,
load_workspace(state, &headers, inputs).await,
StatusCode::UNPROCESSABLE_ENTITY,
)
}
}
}
pub(crate) async fn preview(
State(state): State<AppState>,
headers: HeaderMap,
Form(form): Form<PreviewForm>,
) -> Response {
if let Some(rejection) = reject_cross_site(&headers) {
return rejection;
}
let mut inputs = WorkspaceInputs::for_profile(form.profile.clone());
inputs.preview_form = form.clone();
let request = match form.request() {
Ok(request) => request,
Err(key) => {
inputs.error = Some(crate::tr!(crate::i18n::Locale::from_headers(&headers), key));
return render_loaded(
&headers,
load_workspace(state, &headers, inputs).await,
StatusCode::UNPROCESSABLE_ENTITY,
);
}
};
let Ok(request) = authenticated_request(&headers, request) else {
return Redirect::to("/login").into_response();
};
let mut exchange_rates = state.exchange_rates.clone();
match exchange_rates.preview_direct_conversion(request).await {
Ok(response) => {
let response = response.into_inner();
let locale = crate::i18n::Locale::from_headers(&headers);
inputs.preview = Some(PreviewView {
applied_method: crate::tr!(locale, rate_method_key(response.applied_source)),
applied_date_rule: crate::tr!(locale, date_rule_key(response.applied_date_rule)),
response,
});
render_loaded(
&headers,
load_workspace(state, &headers, inputs).await,
StatusCode::OK,
)
}
Err(error) => {
inputs.error = Some(error.message().to_string());
render_loaded(
&headers,
load_workspace(state, &headers, inputs).await,
StatusCode::UNPROCESSABLE_ENTITY,
)
}
}
}
pub(crate) async fn evidence(
State(state): State<AppState>,
headers: HeaderMap,
Form(form): Form<EvidenceForm>,
) -> Response {
if let Some(rejection) = reject_cross_site(&headers) {
return rejection;
}
let mut inputs = WorkspaceInputs::for_profile(form.profile.clone());
inputs.evidence_form = form.clone();
let record_id = match form.record_id.trim().parse::<i64>() {
Ok(id) if id > 0 => id,
_ => {
inputs.error = Some(crate::tr!(
crate::i18n::Locale::from_headers(&headers),
"rates-err-record-id"
));
return render_loaded(
&headers,
load_workspace(state, &headers, inputs).await,
StatusCode::UNPROCESSABLE_ENTITY,
);
}
};
let request = ListConversionEvidenceRequest {
profile_name: form.profile,
table_name: form.table_name.trim().to_string(),
record_id,
column_name: form.column_name.trim().to_string(),
limit: 50,
before_evidence_id: None,
};
let Ok(request) = authenticated_request(&headers, request) else {
return Redirect::to("/login").into_response();
};
let mut exchange_rates = state.exchange_rates.clone();
match exchange_rates.list_conversion_evidence(request).await {
Ok(response) => {
let response = response.into_inner();
inputs.evidence_has_exchange = Some(response.has_exchange);
inputs.evidence_has_more = response.has_more;
inputs.evidence = response
.evidence
.into_iter()
.map(|evidence| EvidenceView { evidence })
.collect();
render_loaded(
&headers,
load_workspace(state, &headers, inputs).await,
StatusCode::OK,
)
}
Err(error) => {
inputs.error = Some(error.message().to_string());
render_loaded(
&headers,
load_workspace(state, &headers, inputs).await,
StatusCode::UNPROCESSABLE_ENTITY,
)
}
}
}
fn rate_method_key(value: i32) -> &'static str {
match ExchangeRateSource::try_from(value).ok() {
Some(ExchangeRateSource::Official) => "rates-source-official",
Some(ExchangeRateSource::SavedCustom) => "rates-source-saved",
Some(ExchangeRateSource::Manual) => "rates-source-manual",
_ => "rates-unknown",
}
}
fn date_rule_key(value: i32) -> &'static str {
match ExchangeRateDateRule::try_from(value).ok() {
Some(ExchangeRateDateRule::PreviousPublication) => "rates-rule-previous",
Some(ExchangeRateDateRule::OnOrBeforeDate) => "rates-rule-on-or-before",
Some(ExchangeRateDateRule::SpecificPublicationDate) => "rates-rule-specific",
_ => "rates-unknown",
}
}
fn render_loaded(
headers: &HeaderMap,
result: Result<super::workspace_state::ExchangeRateWorkspaceState, LoadError>,
success_status: StatusCode,
) -> Response {
match result {
Ok(page) => (success_status, Html(workspace_ui::render_page(&page))).into_response(),
Err(LoadError::Unauthenticated) => Redirect::to("/login").into_response(),
Err(LoadError::Forbidden) => (
StatusCode::FORBIDDEN,
Html(workspace_ui::render_error(
crate::i18n::Locale::from_headers(headers),
&crate::tr!(crate::i18n::Locale::from_headers(headers), "ecb-forbidden"),
)),
)
.into_response(),
Err(LoadError::Backend(message)) => (
StatusCode::BAD_GATEWAY,
Html(workspace_ui::render_error(
crate::i18n::Locale::from_headers(headers),
&message,
)),
)
.into_response(),
}
}