backbone_accounting/domain/
gl_posting.rs1use chrono::{DateTime, NaiveDate, Utc};
17use rust_decimal::Decimal;
18use serde::{Deserialize, Serialize};
19use uuid::Uuid;
20
21use crate::domain::repositories::budget_control::BudgetBreach;
22
23#[derive(Debug, Clone)]
25pub struct PostingLine {
26 pub account_id: Uuid,
27 pub debit: Decimal,
28 pub credit: Decimal,
29 pub party_type: Option<String>, pub party_id: Option<Uuid>,
31 pub cost_center_id: Option<Uuid>,
32 pub project_id: Option<Uuid>,
33 pub department_id: Option<Uuid>,
34 pub description: Option<String>,
35}
36
37#[derive(Debug, Clone)]
39pub struct PostingRequest {
40 pub company_id: Uuid,
43 pub branch_id: Option<Uuid>,
44 pub source_type: String, pub source_id: Uuid,
46 pub source_reference: Option<String>,
47 pub posting_date: NaiveDate,
48 pub currency: String,
49 pub posting_type: String, pub reverses_post_id: Option<Uuid>,
51 pub description: Option<String>,
52 pub lines: Vec<PostingLine>,
53 pub idempotency_key: Option<String>,
60}
61
62impl PostingRequest {
63 pub fn original(
66 company_id: Uuid,
67 source_type: &str,
68 source_id: Uuid,
69 posting_date: NaiveDate,
70 ) -> Self {
71 Self {
72 company_id,
73 branch_id: None,
74 source_type: source_type.to_string(),
75 source_id,
76 source_reference: None,
77 posting_date,
78 currency: "IDR".to_string(),
79 posting_type: "original".to_string(),
80 reverses_post_id: None,
81 description: None,
82 lines: Vec::new(),
83 idempotency_key: None,
84 }
85 }
86
87 pub fn with_idempotency_key(mut self, key: impl Into<String>) -> Self {
89 self.idempotency_key = Some(key.into());
90 self
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct PostingResult {
97 pub post_id: Uuid,
98 pub journal_id: Uuid,
99 pub posting_status: String,
100 pub idempotent_reuse: bool,
102}
103
104#[derive(Debug)]
108pub enum PostingError {
109 TooFewLines,
110 Unbalanced,
111 InvalidLineAmount,
114 NonPostableAccount(String),
115 AccountNotFound(Uuid),
116 PartyRequired(String),
117 PartyNotAllowed(String),
118 PeriodClosed,
119 BudgetExceeded(Vec<BudgetBreach>),
122 Conflict(String),
123 Internal(String),
124}
125
126impl PostingError {
127 pub fn code(&self) -> &'static str {
128 match self {
129 PostingError::TooFewLines => "too_few_lines",
130 PostingError::Unbalanced => "unbalanced",
131 PostingError::InvalidLineAmount => "invalid_line_amount",
132 PostingError::NonPostableAccount(_) => "non_postable_account",
133 PostingError::AccountNotFound(_) => "account_not_found",
134 PostingError::PartyRequired(_) => "party_required",
135 PostingError::PartyNotAllowed(_) => "party_not_allowed",
136 PostingError::PeriodClosed => "period_closed",
137 PostingError::BudgetExceeded(_) => "budget_exceeded",
138 PostingError::Conflict(_) => "conflict",
139 PostingError::Internal(_) => "internal_error",
140 }
141 }
142
143 pub fn http_status(&self) -> u16 {
145 match self {
146 PostingError::AccountNotFound(_) => 404,
147 PostingError::Internal(_) => 500,
148 _ => 422,
149 }
150 }
151}
152
153impl std::fmt::Display for PostingError {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 write!(f, "{}", self.code())
156 }
157}
158impl std::error::Error for PostingError {}
159
160pub trait PostingEventSink: Send + Sync {
163 fn publish(&self, event: PostingEvent);
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct AccountingPostPosted {
169 pub post_id: Uuid,
170 pub journal_id: Uuid,
171 pub company_id: Uuid,
173 pub source_type: String,
174 pub source_id: Uuid,
175 pub total_debit: Decimal,
176 pub total_credit: Decimal,
177 pub occurred_at: DateTime<Utc>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct AccountingPostFailed {
183 pub company_id: Uuid,
185 pub source_type: String,
186 pub source_id: Uuid,
187 pub error_code: String,
188 pub error_message: String,
189 pub occurred_at: DateTime<Utc>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(tag = "type")]
195pub enum PostingEvent {
196 AccountingPostPosted(AccountingPostPosted),
197 AccountingPostFailed(AccountingPostFailed),
198}
199
200pub fn map_source(source_type: &str, posting_type: &str) -> (&'static str, &'static str) {
202 if posting_type == "reversal" {
203 return ("reversing", "adjustment");
204 }
205 match source_type {
206 "order" => ("sales", "order"),
207 "payment" => ("cash_receipt", "payment"),
208 "settlement" => ("general", "settlement"),
209 "refund" => ("general", "adjustment"),
210 "expense" => ("purchase", "adjustment"),
211 "inventory" => ("general", "system"),
212 "reconciliation" => ("general", "system"),
214 "gateway_fee" => ("general", "system"),
216 _ => ("general", "manual"),
217 }
218}