backbone_accounting/presentation/http/
accounting_ops_handler.rs1use std::sync::Arc;
22
23use axum::{
24 extract::{Path, State},
25 http::StatusCode,
26 response::IntoResponse,
27 routing::post,
28 Json, Router,
29};
30use serde::Deserialize;
31use uuid::Uuid;
32
33use crate::application::service::bank_reconciliation_service::{
34 BankReconciliationService, ReconcileRequest,
35};
36use crate::application::service::period_close_service::{PeriodCloseError, PeriodCloseService};
37use backbone_auth::org::OrgContext;
38
39async fn reconcile(
41 State(svc): State<Arc<BankReconciliationService>>,
42 Json(req): Json<ReconcileRequest>,
43) -> impl IntoResponse {
44 match svc.reconcile(req).await {
45 Ok(r) => (StatusCode::OK, Json(serde_json::to_value(r).unwrap())),
46 Err(e) => (
47 StatusCode::UNPROCESSABLE_ENTITY,
48 Json(serde_json::json!({ "success": false, "error": e.to_string() })),
49 ),
50 }
51}
52
53pub fn create_bank_reconciliation_routes(service: Arc<BankReconciliationService>) -> Router {
54 Router::new()
55 .route("/accounting/reconcile", post(reconcile))
56 .with_state(service)
57}
58
59#[derive(Debug, Deserialize)]
61pub struct ClosePeriodBody {
62 pub retained_earnings_account_id: Uuid,
63}
64
65#[derive(Debug, Deserialize)]
67pub struct PeriodReasonBody {
68 #[serde(default)]
69 pub reason: String,
70}
71
72fn actor(org: &OrgContext) -> Option<Uuid> {
73 Uuid::parse_str(&org.user_id).ok()
74}
75
76fn period_error(e: PeriodCloseError) -> (StatusCode, Json<serde_json::Value>) {
80 let status = match &e {
81 PeriodCloseError::PeriodNotFound(_) => StatusCode::NOT_FOUND,
82 PeriodCloseError::AlreadyClosed
83 | PeriodCloseError::CloseInProgress
84 | PeriodCloseError::NotClosed { .. }
85 | PeriodCloseError::Locked => StatusCode::CONFLICT,
86 PeriodCloseError::ReasonRequired | PeriodCloseError::Posting(_) => StatusCode::UNPROCESSABLE_ENTITY,
87 PeriodCloseError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
88 };
89 (status, Json(serde_json::json!({ "success": false, "error": e.to_string() })))
90}
91
92async fn close_period(
93 State(svc): State<Arc<PeriodCloseService>>,
94 org: OrgContext,
95 Path(period_id): Path<Uuid>,
96 Json(body): Json<ClosePeriodBody>,
97) -> impl IntoResponse {
98 match svc
99 .close_period_as(period_id, body.retained_earnings_account_id, actor(&org))
100 .await
101 {
102 Ok(r) => (StatusCode::OK, Json(serde_json::to_value(r).unwrap())),
103 Err(e) => period_error(e),
104 }
105}
106
107async fn lock_period(
108 State(svc): State<Arc<PeriodCloseService>>,
109 org: OrgContext,
110 Path(period_id): Path<Uuid>,
111 Json(body): Json<PeriodReasonBody>,
112) -> impl IntoResponse {
113 match svc.lock_period(period_id, actor(&org), &body.reason).await {
114 Ok(()) => (StatusCode::OK, Json(serde_json::json!({ "success": true, "period_id": period_id, "status": "locked" }))),
115 Err(e) => period_error(e),
116 }
117}
118
119async fn reopen_period(
120 State(svc): State<Arc<PeriodCloseService>>,
121 org: OrgContext,
122 Path(period_id): Path<Uuid>,
123 Json(body): Json<PeriodReasonBody>,
124) -> impl IntoResponse {
125 match svc.reopen_period(period_id, actor(&org), &body.reason).await {
126 Ok(()) => (StatusCode::OK, Json(serde_json::json!({ "success": true, "period_id": period_id, "status": "open" }))),
127 Err(e) => period_error(e),
128 }
129}
130
131pub fn create_period_close_routes(service: Arc<PeriodCloseService>) -> Router {
135 Router::new()
136 .route("/accounting/periods/:period_id/close", post(close_period))
137 .with_state(service)
138}
139
140pub fn create_period_finality_routes(service: Arc<PeriodCloseService>) -> Router {
144 Router::new()
145 .route("/accounting/periods/:period_id/lock", post(lock_period))
146 .route("/accounting/periods/:period_id/reopen", post(reopen_period))
147 .with_state(service)
148}