backbone_accounting/presentation/http/
reconcile_handler.rs1use std::sync::Arc;
21
22use axum::{
23 extract::{Path, Query, State},
24 http::StatusCode,
25 response::IntoResponse,
26 routing::{get, post},
27 Json, Router,
28};
29use rust_decimal::Decimal;
30use serde::{Deserialize, Serialize};
31use uuid::Uuid;
32
33use crate::application::service::reconcile_write_service::ReconcileWriteService;
34use crate::domain::reconcile_graph::{
35 EdgeOutcome, LineLocator, PairRequest, ReconcileError, ORIGIN_MANUAL,
36};
37
38#[derive(Debug, Deserialize)]
43pub struct ReconcileLineDto {
44 pub source_type: String,
46 pub source_id: Uuid,
48 pub account_id: Uuid,
50 #[serde(default)]
52 pub reversing: bool,
53}
54
55#[derive(Debug, Deserialize)]
56pub struct ReconcilePairRequestDto {
57 pub debit: ReconcileLineDto,
58 pub credit: ReconcileLineDto,
59 pub amount: Decimal,
61 #[serde(default = "default_origin")]
63 pub origin: String,
64}
65
66fn default_origin() -> String {
67 ORIGIN_MANUAL.to_string()
68}
69
70#[derive(Debug, Serialize)]
71pub struct ReconcileEdgeAckDto {
72 pub partial_id: Option<Uuid>,
74 pub applied: Decimal,
75 pub full_reconcile_id: Option<Uuid>,
76}
77
78#[derive(Debug, Deserialize)]
79pub struct UnreconcileBody {
80}
81
82#[derive(Debug, Deserialize)]
83pub struct CompanyQuery {
84}
85
86#[derive(Debug, Serialize)]
87pub struct MatchingGroupDto {
88 pub label: String,
89 pub full_reconcile_id: Option<Uuid>,
90 pub line_ids: Vec<Uuid>,
91 pub partial_ids: Vec<Uuid>,
92 pub residuals: Vec<ResidualDto>,
93}
94
95#[derive(Debug, Serialize)]
96pub struct ResidualDto {
97 pub line_id: Uuid,
98 pub residual: Decimal,
99}
100
101#[derive(Debug, Serialize)]
102pub struct ReconcileErrorDto {
103 pub code: String,
104 pub message: String,
105}
106
107fn locator_from_dto(dto: ReconcileLineDto) -> LineLocator {
112 let l = LineLocator::new(&dto.source_type, dto.source_id, dto.account_id);
113 if dto.reversing {
114 l.reversing()
115 } else {
116 l
117 }
118}
119
120fn ack_from_outcome(o: EdgeOutcome) -> ReconcileEdgeAckDto {
121 ReconcileEdgeAckDto {
122 partial_id: o.partial_id,
123 applied: o.applied,
124 full_reconcile_id: o.full_reconcile_id,
125 }
126}
127
128fn error_response(e: &ReconcileError) -> axum::response::Response {
129 let status = StatusCode::from_u16(e.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
130 (
131 status,
132 Json(ReconcileErrorDto {
133 code: e.code().to_string(),
134 message: e.to_string(),
135 }),
136 )
137 .into_response()
138}
139
140async fn reconcile_handler(
159 State(service): State<Arc<ReconcileWriteService>>,
160 Json(dto): Json<ReconcilePairRequestDto>,
161) -> impl IntoResponse {
162 let req = PairRequest {
163 debit: locator_from_dto(dto.debit),
164 credit: locator_from_dto(dto.credit),
165 amount: dto.amount,
166 origin: dto.origin,
167 actor: None,
168 };
169 match service.reconcile_pair(&req).await {
170 Ok(o) => (StatusCode::OK, Json(ack_from_outcome(o))).into_response(),
171 Err(e) => error_response(&e),
172 }
173}
174
175async fn unreconcile_handler(
176 State(service): State<Arc<ReconcileWriteService>>,
177 Path(partial_id): Path<Uuid>,
178 Json(_body): Json<UnreconcileBody>,
179) -> impl IntoResponse {
180 match service.unreconcile(partial_id, None).await {
181 Ok(()) => StatusCode::NO_CONTENT.into_response(),
182 Err(e) => error_response(&e),
183 }
184}
185
186async fn matching_group_handler(
187 State(service): State<Arc<ReconcileWriteService>>,
188 Path(line_id): Path<Uuid>,
189 Query(_q): Query<CompanyQuery>,
190) -> impl IntoResponse {
191 match service.matching_group(line_id).await {
192 Ok(g) => (
193 StatusCode::OK,
194 Json(MatchingGroupDto {
195 label: g.label,
196 full_reconcile_id: g.full_reconcile_id,
197 line_ids: g.line_ids,
198 partial_ids: g.partial_ids,
199 residuals: g
200 .residuals
201 .into_iter()
202 .map(|(line_id, residual)| ResidualDto { line_id, residual })
203 .collect(),
204 }),
205 )
206 .into_response(),
207 Err(e) => error_response(&e),
208 }
209}
210
211pub fn create_reconcile_verb_routes(service: Arc<ReconcileWriteService>) -> Router {
219 Router::new()
220 .route("/accounting/reconcile", post(reconcile_handler))
221 .route(
222 "/accounting/unreconcile/:partial_id",
223 post(unreconcile_handler),
224 )
225 .route(
226 "/accounting/reconciliation-groups/:line_id",
227 get(matching_group_handler),
228 )
229 .with_state(service)
230}