use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::domain::repositories::budget_control::BudgetBreach;
#[derive(Debug, Clone)]
pub struct PostingLine {
pub account_id: Uuid,
pub debit: Decimal,
pub credit: Decimal,
pub party_type: Option<String>, pub party_id: Option<Uuid>,
pub cost_center_id: Option<Uuid>,
pub project_id: Option<Uuid>,
pub department_id: Option<Uuid>,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PostingRequest {
pub company_id: Uuid,
pub branch_id: Option<Uuid>,
pub source_type: String, pub source_id: Uuid,
pub source_reference: Option<String>,
pub posting_date: NaiveDate,
pub currency: String,
pub posting_type: String, pub reverses_post_id: Option<Uuid>,
pub description: Option<String>,
pub lines: Vec<PostingLine>,
pub idempotency_key: Option<String>,
}
impl PostingRequest {
pub fn original(
company_id: Uuid,
source_type: &str,
source_id: Uuid,
posting_date: NaiveDate,
) -> Self {
Self {
company_id,
branch_id: None,
source_type: source_type.to_string(),
source_id,
source_reference: None,
posting_date,
currency: "IDR".to_string(),
posting_type: "original".to_string(),
reverses_post_id: None,
description: None,
lines: Vec::new(),
idempotency_key: None,
}
}
pub fn with_idempotency_key(mut self, key: impl Into<String>) -> Self {
self.idempotency_key = Some(key.into());
self
}
}
#[derive(Debug, Clone)]
pub struct PostingResult {
pub post_id: Uuid,
pub journal_id: Uuid,
pub posting_status: String,
pub idempotent_reuse: bool,
}
#[derive(Debug)]
pub enum PostingError {
TooFewLines,
Unbalanced,
InvalidLineAmount,
NonPostableAccount(String),
AccountNotFound(Uuid),
PartyRequired(String),
PartyNotAllowed(String),
PeriodClosed,
BudgetExceeded(Vec<BudgetBreach>),
Conflict(String),
Internal(String),
}
impl PostingError {
pub fn code(&self) -> &'static str {
match self {
PostingError::TooFewLines => "too_few_lines",
PostingError::Unbalanced => "unbalanced",
PostingError::InvalidLineAmount => "invalid_line_amount",
PostingError::NonPostableAccount(_) => "non_postable_account",
PostingError::AccountNotFound(_) => "account_not_found",
PostingError::PartyRequired(_) => "party_required",
PostingError::PartyNotAllowed(_) => "party_not_allowed",
PostingError::PeriodClosed => "period_closed",
PostingError::BudgetExceeded(_) => "budget_exceeded",
PostingError::Conflict(_) => "conflict",
PostingError::Internal(_) => "internal_error",
}
}
pub fn http_status(&self) -> u16 {
match self {
PostingError::AccountNotFound(_) => 404,
PostingError::Internal(_) => 500,
_ => 422,
}
}
}
impl std::fmt::Display for PostingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.code())
}
}
impl std::error::Error for PostingError {}
pub trait PostingEventSink: Send + Sync {
fn publish(&self, event: PostingEvent);
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountingPostPosted {
pub post_id: Uuid,
pub journal_id: Uuid,
pub company_id: Uuid,
pub source_type: String,
pub source_id: Uuid,
pub total_debit: Decimal,
pub total_credit: Decimal,
pub occurred_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountingPostFailed {
pub company_id: Uuid,
pub source_type: String,
pub source_id: Uuid,
pub error_code: String,
pub error_message: String,
pub occurred_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum PostingEvent {
AccountingPostPosted(AccountingPostPosted),
AccountingPostFailed(AccountingPostFailed),
}
pub fn map_source(source_type: &str, posting_type: &str) -> (&'static str, &'static str) {
if posting_type == "reversal" {
return ("reversing", "adjustment");
}
match source_type {
"order" => ("sales", "order"),
"payment" => ("cash_receipt", "payment"),
"settlement" => ("general", "settlement"),
"refund" => ("general", "adjustment"),
"expense" => ("purchase", "adjustment"),
"inventory" => ("general", "system"),
"reconciliation" => ("general", "system"),
"gateway_fee" => ("general", "system"),
_ => ("general", "manual"),
}
}