use std::sync::Arc;
use chrono::Utc;
use uuid::Uuid;
use crate::application::service::posting_service::PostingService;
use crate::domain::gl_posting::{PostingError, PostingRequest};
use crate::domain::repositories::journal_workflow_repository::JournalWorkflowRepository;
use crate::domain::repositories::posting_repository::PostingRepository;
#[derive(Debug)]
pub enum JournalWorkflowError {
NotFound(Uuid),
InvalidState {
id: Uuid,
current: String,
expected: &'static str,
},
NotPosted(Uuid),
Posting(PostingError),
Internal(String),
}
impl JournalWorkflowError {
pub fn code(&self) -> &'static str {
match self {
Self::NotFound(_) => "journal_not_found",
Self::InvalidState { .. } => "invalid_journal_state",
Self::NotPosted(_) => "journal_not_posted",
Self::Posting(_) => "posting_error",
Self::Internal(_) => "internal_error",
}
}
pub fn http_status(&self) -> u16 {
match self {
Self::NotFound(_) => 404,
Self::Internal(_) => 500,
Self::Posting(e) => e.http_status(),
_ => 422,
}
}
}
impl std::fmt::Display for JournalWorkflowError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(id) => write!(f, "journal_not_found: {id}"),
Self::InvalidState {
id,
current,
expected,
} => {
write!(
f,
"invalid_journal_state: {id} is '{current}', expected '{expected}'"
)
}
Self::NotPosted(id) => write!(f, "journal_not_posted: {id}"),
Self::Posting(e) => write!(f, "posting_error: {e}"),
Self::Internal(e) => write!(f, "internal_error: {e}"),
}
}
}
impl std::error::Error for JournalWorkflowError {}
impl From<PostingError> for JournalWorkflowError {
fn from(e: PostingError) -> Self {
Self::Posting(e)
}
}
fn internal(e: anyhow::Error) -> JournalWorkflowError {
JournalWorkflowError::Internal(e.to_string())
}
#[derive(Clone)]
pub struct JournalWorkflowService {
workflow: Arc<dyn JournalWorkflowRepository>,
posting: PostingService,
}
impl JournalWorkflowService {
pub fn new(
posting_repo: Arc<dyn PostingRepository>,
workflow: Arc<dyn JournalWorkflowRepository>,
) -> Self {
Self {
posting: PostingService::new(posting_repo),
workflow,
}
}
pub fn with_budget_control_if_set(
mut self,
port: Option<std::sync::Arc<dyn crate::domain::repositories::BudgetControlPort>>,
) -> Self {
self.posting = self.posting.with_budget_control_if_set(port);
self
}
pub async fn submit(
&self,
journal_id: Uuid,
) -> Result<(), JournalWorkflowError> {
let ok = self
.workflow
.submit(journal_id)
.await
.map_err(internal)?;
if !ok {
return Err(self.state_error(journal_id, "draft").await);
}
Ok(())
}
pub async fn approve(
&self,
journal_id: Uuid,
approved_by: Option<Uuid>,
) -> Result<crate::domain::gl_posting::PostingResult, JournalWorkflowError> {
let now = Utc::now();
let ok = self
.workflow
.approve(journal_id, approved_by, now)
.await
.map_err(internal)?;
if !ok {
return Err(self
.state_error(journal_id, "pending_approval")
.await);
}
Ok(self
.posting
.post_journal(
journal_id,
backbone_orm::org_scope::current_org_scope()
.and_then(|sc| sc.legacy_company_id())
.unwrap_or_default(),
approved_by,
)
.await?)
}
pub async fn reject(
&self,
journal_id: Uuid,
reason: String,
rejected_by: Option<Uuid>,
) -> Result<(), JournalWorkflowError> {
let now = Utc::now();
let ok = self
.workflow
.reject(journal_id, &reason, rejected_by, now)
.await
.map_err(internal)?;
if !ok {
return Err(self
.state_error(journal_id, "draft or pending_approval")
.await);
}
Ok(())
}
pub async fn void(
&self,
journal_id: Uuid,
voided_by: Option<Uuid>,
reason: String,
) -> Result<crate::domain::gl_posting::PostingResult, JournalWorkflowError> {
let Some(status) = self
.workflow
.find_status(journal_id)
.await
.map_err(internal)?
else {
return Err(JournalWorkflowError::NotFound(journal_id));
};
if status.status != "posted" {
return Err(JournalWorkflowError::InvalidState {
id: journal_id,
current: status.status,
expected: "posted",
});
}
let Some(orig_post_id) = self
.workflow
.original_post(journal_id)
.await
.map_err(internal)?
else {
return Err(JournalWorkflowError::NotPosted(journal_id));
};
let req = PostingRequest {
company_id: backbone_orm::org_scope::current_org_scope()
.and_then(|sc| sc.legacy_company_id())
.unwrap_or_default(),
branch_id: None,
source_type: "manual".to_string(),
source_id: journal_id,
source_reference: None,
posting_date: Utc::now().date_naive(),
currency: status.currency,
posting_type: "reversal".to_string(),
reverses_post_id: Some(orig_post_id),
description: Some(format!("Void of journal {journal_id}: {reason}")),
lines: Vec::new(), idempotency_key: Some(format!("void:{journal_id}")),
};
let result = self.posting.post(req, voided_by).await?;
let now = Utc::now();
self.workflow
.mark_voided(journal_id, voided_by, &reason, now)
.await
.map_err(internal)?;
Ok(result)
}
async fn state_error(
&self,
journal_id: Uuid,
expected: &'static str,
) -> JournalWorkflowError {
match self.workflow.current_status(journal_id).await {
Ok(None) => JournalWorkflowError::NotFound(journal_id),
Ok(Some(current)) => JournalWorkflowError::InvalidState {
id: journal_id,
current,
expected,
},
Err(e) => internal(e),
}
}
}