Skip to main content

backbone_accounting/presentation/http/
check_printing_handler.rs

1//! Check printing HTTP surface — hand-authored (user-owned; see
2//! `metaphor.codegen.yaml`).
3//!
4//! Four routes over the `CheckPrintingService`:
5//! - `PUT    /accounting/checks/sequences`        — register/replace a
6//!   bank-journal sequence (auto/manual mode, starting number)
7//! - `POST   /accounting/checks/allocate`         — allocate the next number(s)
8//!   from an AUTO sequence (atomic; distinct under concurrency)
9//! - `POST   /accounting/checks`                  — record one printed check
10//!   (auto: allocates inside the verb transaction; manual: validates + unique)
11//! - `POST   /accounting/checks/:id/void`         — void a printed check (the
12//!   number stays consumed)
13//!
14//! Hosts mount these behind their own auth layers; the registry verbs belong
15//! behind an accounting-officer role gate at the host. The tenant-consistency
16//! check refuses a body company that disagrees with the ambient scope.
17//!
18//! Tenancy (ADR-0029): the `company_id` fields in the wire bodies are the legacy
19//! twin — kept so unstripped callers compile and run unchanged; the module keys
20//! no statement on them and the composing service's tenancy decorator scopes the
21//! underlying reads/writes.
22
23use std::sync::Arc;
24
25use axum::{
26    extract::{Path, State},
27    http::StatusCode,
28    response::IntoResponse,
29    routing::{post, put},
30    Json, Router,
31};
32use rust_decimal::Decimal;
33use serde::{Deserialize, Serialize};
34use uuid::Uuid;
35
36use crate::application::service::check_printing_service::{
37    AllocationAck, CheckPrintingError, CheckPrintingService, PrintedCheckAck,
38    RecordCheck, RegisterSequence, SequenceAck,
39};
40
41#[derive(Debug, Deserialize)]
42pub struct RegisterSequenceBody {
43    pub bank_account_id: Uuid,
44    /// "auto" | "manual".
45    pub numbering_mode: String,
46    #[serde(default = "default_next")]
47    pub next_number: i64,
48}
49
50fn default_next() -> i64 {
51    1
52}
53
54#[derive(Debug, Deserialize)]
55pub struct AllocateBody {
56    pub bank_account_id: Uuid,
57    #[serde(default = "default_count")]
58    pub count: i64,
59}
60
61fn default_count() -> i64 {
62    1
63}
64
65#[derive(Debug, Deserialize)]
66pub struct RecordCheckBody {
67    pub bank_account_id: Uuid,
68    pub payment_id: Uuid,
69    #[serde(default)]
70    pub payment_number: Option<String>,
71    pub amount: Decimal,
72    #[serde(default)]
73    pub payee_name: Option<String>,
74    /// Manual numbering only; must be absent for auto sequences.
75    #[serde(default)]
76    pub check_number: Option<String>,
77    #[serde(default)]
78    pub actor: Option<Uuid>,
79}
80
81#[derive(Debug, Deserialize)]
82pub struct VoidBody {
83    #[serde(default)]
84    pub actor: Option<Uuid>,
85}
86
87#[derive(Debug, Serialize)]
88struct ErrorBody {
89    error: String,
90    message: String,
91}
92
93fn error_response(e: &CheckPrintingError) -> axum::response::Response {
94    let status = StatusCode::from_u16(e.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
95    (
96        status,
97        Json(ErrorBody {
98            error: e.code().to_string(),
99            message: e.to_string(),
100        }),
101    )
102        .into_response()
103}
104
105
106
107async fn register_sequence(
108    State(service): State<Arc<CheckPrintingService>>,
109    Json(body): Json<RegisterSequenceBody>,
110) -> impl IntoResponse {
111    let req = RegisterSequence {
112        bank_account_id: body.bank_account_id,
113        numbering_mode: body.numbering_mode,
114        next_number: body.next_number,
115    };
116    match service.register_sequence(req).await {
117        Ok(ack) => (StatusCode::OK, Json(SequenceAck {
118            sequence_id: ack.sequence_id,
119            bank_account_id: ack.bank_account_id,
120            numbering_mode: ack.numbering_mode,
121            next_number: ack.next_number,
122        }))
123            .into_response(),
124        Err(e) => error_response(&e),
125    }
126}
127
128async fn allocate(
129    State(service): State<Arc<CheckPrintingService>>,
130    Json(body): Json<AllocateBody>,
131) -> impl IntoResponse {
132    match service
133        .allocate_check_numbers(body.bank_account_id, body.count)
134        .await
135    {
136        Ok(ack) => (StatusCode::OK, Json(AllocationAck {
137            first: ack.first,
138            last: ack.last,
139            numbers: ack.numbers,
140        }))
141            .into_response(),
142        Err(e) => error_response(&e),
143    }
144}
145
146async fn record_check(
147    State(service): State<Arc<CheckPrintingService>>,
148    Json(body): Json<RecordCheckBody>,
149) -> impl IntoResponse {
150    let req = RecordCheck {
151        bank_account_id: body.bank_account_id,
152        payment_id: body.payment_id,
153        payment_number: body.payment_number,
154        amount: body.amount,
155        payee_name: body.payee_name,
156        check_number: body.check_number,
157        actor: body.actor,
158    };
159    match service.record_printed_check(req).await {
160        Ok(ack) => (StatusCode::CREATED, Json(PrintedCheckAck {
161            printed_check_id: ack.printed_check_id,
162            bank_account_id: ack.bank_account_id,
163            payment_id: ack.payment_id,
164            check_number: ack.check_number,
165            status: ack.status,
166        }))
167            .into_response(),
168        Err(e) => error_response(&e),
169    }
170}
171
172async fn void_check(
173    State(service): State<Arc<CheckPrintingService>>,
174    Path(printed_check_id): Path<Uuid>,
175    Json(body): Json<VoidBody>,
176) -> impl IntoResponse {
177    match service
178        .void_printed_check(printed_check_id, body.actor)
179        .await
180    {
181        Ok(ack) => (StatusCode::OK, Json(PrintedCheckAck {
182            printed_check_id: ack.printed_check_id,
183            bank_account_id: ack.bank_account_id,
184            payment_id: ack.payment_id,
185            check_number: ack.check_number,
186            status: ack.status,
187        }))
188            .into_response(),
189        Err(e) => error_response(&e),
190    }
191}
192
193/// The check printing routes (sequence registration + allocation + registry +
194/// void).
195pub fn create_check_printing_routes(service: Arc<CheckPrintingService>) -> Router {
196    Router::new()
197        .route("/accounting/checks/sequences", put(register_sequence))
198        .route("/accounting/checks/allocate", post(allocate))
199        .route("/accounting/checks", post(record_check))
200        .route("/accounting/checks/:id/void", post(void_check))
201        .with_state(service)
202}