use std::sync::Arc;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::application::service::reconcile_write_service::ReconcileWriteService;
use crate::domain::reconcile_graph::{
EdgeOutcome, LineLocator, PairRequest, ReconcileError, ORIGIN_MANUAL,
};
#[derive(Debug, Deserialize)]
pub struct ReconcileLineDto {
pub source_type: String,
pub source_id: Uuid,
pub account_id: Uuid,
#[serde(default)]
pub reversing: bool,
}
#[derive(Debug, Deserialize)]
pub struct ReconcilePairRequestDto {
pub debit: ReconcileLineDto,
pub credit: ReconcileLineDto,
pub amount: Decimal,
#[serde(default = "default_origin")]
pub origin: String,
}
fn default_origin() -> String {
ORIGIN_MANUAL.to_string()
}
#[derive(Debug, Serialize)]
pub struct ReconcileEdgeAckDto {
pub partial_id: Option<Uuid>,
pub applied: Decimal,
pub full_reconcile_id: Option<Uuid>,
}
#[derive(Debug, Deserialize)]
pub struct UnreconcileBody {
}
#[derive(Debug, Deserialize)]
pub struct CompanyQuery {
}
#[derive(Debug, Serialize)]
pub struct MatchingGroupDto {
pub label: String,
pub full_reconcile_id: Option<Uuid>,
pub line_ids: Vec<Uuid>,
pub partial_ids: Vec<Uuid>,
pub residuals: Vec<ResidualDto>,
}
#[derive(Debug, Serialize)]
pub struct ResidualDto {
pub line_id: Uuid,
pub residual: Decimal,
}
#[derive(Debug, Serialize)]
pub struct ReconcileErrorDto {
pub code: String,
pub message: String,
}
fn locator_from_dto(dto: ReconcileLineDto) -> LineLocator {
let l = LineLocator::new(&dto.source_type, dto.source_id, dto.account_id);
if dto.reversing {
l.reversing()
} else {
l
}
}
fn ack_from_outcome(o: EdgeOutcome) -> ReconcileEdgeAckDto {
ReconcileEdgeAckDto {
partial_id: o.partial_id,
applied: o.applied,
full_reconcile_id: o.full_reconcile_id,
}
}
fn error_response(e: &ReconcileError) -> axum::response::Response {
let status = StatusCode::from_u16(e.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
(
status,
Json(ReconcileErrorDto {
code: e.code().to_string(),
message: e.to_string(),
}),
)
.into_response()
}
async fn reconcile_handler(
State(service): State<Arc<ReconcileWriteService>>,
Json(dto): Json<ReconcilePairRequestDto>,
) -> impl IntoResponse {
let req = PairRequest {
debit: locator_from_dto(dto.debit),
credit: locator_from_dto(dto.credit),
amount: dto.amount,
origin: dto.origin,
actor: None,
};
match service.reconcile_pair(&req).await {
Ok(o) => (StatusCode::OK, Json(ack_from_outcome(o))).into_response(),
Err(e) => error_response(&e),
}
}
async fn unreconcile_handler(
State(service): State<Arc<ReconcileWriteService>>,
Path(partial_id): Path<Uuid>,
Json(_body): Json<UnreconcileBody>,
) -> impl IntoResponse {
match service.unreconcile(partial_id, None).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => error_response(&e),
}
}
async fn matching_group_handler(
State(service): State<Arc<ReconcileWriteService>>,
Path(line_id): Path<Uuid>,
Query(_q): Query<CompanyQuery>,
) -> impl IntoResponse {
match service.matching_group(line_id).await {
Ok(g) => (
StatusCode::OK,
Json(MatchingGroupDto {
label: g.label,
full_reconcile_id: g.full_reconcile_id,
line_ids: g.line_ids,
partial_ids: g.partial_ids,
residuals: g
.residuals
.into_iter()
.map(|(line_id, residual)| ResidualDto { line_id, residual })
.collect(),
}),
)
.into_response(),
Err(e) => error_response(&e),
}
}
pub fn create_reconcile_verb_routes(service: Arc<ReconcileWriteService>) -> Router {
Router::new()
.route("/accounting/reconcile", post(reconcile_handler))
.route(
"/accounting/unreconcile/:partial_id",
post(unreconcile_handler),
)
.route(
"/accounting/reconciliation-groups/:line_id",
get(matching_group_handler),
)
.with_state(service)
}