use chrono::{DateTime, Utc, NaiveDate};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
use rust_decimal::Decimal;
use super::AccountType;
use super::AccountSubtype;
use super::NormalBalance;
use super::AccountStatus;
use super::AuditMetadata;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AccountId(pub Uuid);
impl AccountId {
pub fn new(id: Uuid) -> Self { Self(id) }
pub fn generate() -> Self { Self(Uuid::new_v4()) }
pub fn into_inner(self) -> Uuid { self.0 }
}
impl std::fmt::Display for AccountId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for AccountId {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Uuid::parse_str(s)?))
}
}
impl From<Uuid> for AccountId {
fn from(id: Uuid) -> Self { Self(id) }
}
impl From<AccountId> for Uuid {
fn from(id: AccountId) -> Self { id.0 }
}
impl AsRef<Uuid> for AccountId {
fn as_ref(&self) -> &Uuid { &self.0 }
}
impl std::ops::Deref for AccountId {
type Target = Uuid;
fn deref(&self) -> &Self::Target { &self.0 }
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Account {
pub id: Uuid,
pub account_number: String,
pub account_code: String,
pub name: String,
pub description: Option<String>,
pub account_type: AccountType,
pub account_subtype: AccountSubtype,
pub normal_balance: NormalBalance,
pub parent_id: Option<Uuid>,
pub level: i32,
pub path: Option<String>,
pub is_header: bool,
pub is_detail: bool,
pub currency: String,
pub opening_balance: Decimal,
pub opening_balance_date: Option<NaiveDate>,
pub current_balance: Decimal,
pub is_reconcilable: bool,
pub has_budget: bool,
pub budget_amount: Option<Decimal>,
pub sort_order: i32,
pub show_in_reports: bool,
pub status: AccountStatus,
pub is_system: bool,
pub notes: Option<String>,
pub source_id: Option<Uuid>,
pub is_cloned: bool,
pub chart_code: Option<String>,
pub chart_version: Option<String>,
#[serde(default)]
#[sqlx(json)]
pub metadata: AuditMetadata,
}
impl Account {
pub fn builder() -> AccountBuilder {
<AccountBuilder as Default>::default()
}
pub fn new(account_number: String, account_code: String, name: String, account_type: AccountType, account_subtype: AccountSubtype, normal_balance: NormalBalance, level: i32, is_header: bool, is_detail: bool, currency: String, opening_balance: Decimal, current_balance: Decimal, is_reconcilable: bool, has_budget: bool, sort_order: i32, show_in_reports: bool, status: AccountStatus, is_system: bool, is_cloned: bool) -> Self {
Self {
id: Uuid::new_v4(),
account_number,
account_code,
name,
description: None,
account_type,
account_subtype,
normal_balance,
parent_id: None,
level,
path: None,
is_header,
is_detail,
currency,
opening_balance,
opening_balance_date: None,
current_balance,
is_reconcilable,
has_budget,
budget_amount: None,
sort_order,
show_in_reports,
status,
is_system,
notes: None,
source_id: None,
is_cloned,
chart_code: None,
chart_version: None,
metadata: AuditMetadata::default(),
}
}
pub fn id(&self) -> &Uuid {
&self.id
}
pub fn typed_id(&self) -> AccountId {
AccountId(self.id)
}
pub fn created_at(&self) -> Option<&DateTime<Utc>> {
self.metadata.created_at.as_ref()
}
pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
self.metadata.updated_at.as_ref()
}
pub fn is_deleted(&self) -> bool {
self.metadata.deleted_at.is_some()
}
pub fn is_active(&self) -> bool {
self.metadata.deleted_at.is_none()
}
pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
self.metadata.deleted_at.as_ref()
}
pub fn created_by(&self) -> Option<&Uuid> {
self.metadata.created_by.as_ref()
}
pub fn updated_by(&self) -> Option<&Uuid> {
self.metadata.updated_by.as_ref()
}
pub fn deleted_by(&self) -> Option<&Uuid> {
self.metadata.deleted_by.as_ref()
}
pub fn status(&self) -> &AccountStatus {
&self.status
}
pub fn with_description(mut self, value: String) -> Self {
self.description = Some(value);
self
}
pub fn with_parent_id(mut self, value: Uuid) -> Self {
self.parent_id = Some(value);
self
}
pub fn with_path(mut self, value: String) -> Self {
self.path = Some(value);
self
}
pub fn with_opening_balance_date(mut self, value: NaiveDate) -> Self {
self.opening_balance_date = Some(value);
self
}
pub fn with_budget_amount(mut self, value: Decimal) -> Self {
self.budget_amount = Some(value);
self
}
pub fn with_notes(mut self, value: String) -> Self {
self.notes = Some(value);
self
}
pub fn with_source_id(mut self, value: Uuid) -> Self {
self.source_id = Some(value);
self
}
pub fn with_chart_code(mut self, value: String) -> Self {
self.chart_code = Some(value);
self
}
pub fn with_chart_version(mut self, value: String) -> Self {
self.chart_version = Some(value);
self
}
pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
for (key, value) in fields {
match key.as_str() {
"account_number" => {
if let Ok(v) = serde_json::from_value(value) { self.account_number = v; }
}
"account_code" => {
if let Ok(v) = serde_json::from_value(value) { self.account_code = v; }
}
"name" => {
if let Ok(v) = serde_json::from_value(value) { self.name = v; }
}
"description" => {
if let Ok(v) = serde_json::from_value(value) { self.description = v; }
}
"account_type" => {
if let Ok(v) = serde_json::from_value(value) { self.account_type = v; }
}
"account_subtype" => {
if let Ok(v) = serde_json::from_value(value) { self.account_subtype = v; }
}
"normal_balance" => {
if let Ok(v) = serde_json::from_value(value) { self.normal_balance = v; }
}
"parent_id" => {
if let Ok(v) = serde_json::from_value(value) { self.parent_id = v; }
}
"level" => {
if let Ok(v) = serde_json::from_value(value) { self.level = v; }
}
"path" => {
if let Ok(v) = serde_json::from_value(value) { self.path = v; }
}
"is_header" => {
if let Ok(v) = serde_json::from_value(value) { self.is_header = v; }
}
"is_detail" => {
if let Ok(v) = serde_json::from_value(value) { self.is_detail = v; }
}
"currency" => {
if let Ok(v) = serde_json::from_value(value) { self.currency = v; }
}
"opening_balance" => {
if let Ok(v) = serde_json::from_value(value) { self.opening_balance = v; }
}
"opening_balance_date" => {
if let Ok(v) = serde_json::from_value(value) { self.opening_balance_date = v; }
}
"current_balance" => {
if let Ok(v) = serde_json::from_value(value) { self.current_balance = v; }
}
"is_reconcilable" => {
if let Ok(v) = serde_json::from_value(value) { self.is_reconcilable = v; }
}
"has_budget" => {
if let Ok(v) = serde_json::from_value(value) { self.has_budget = v; }
}
"budget_amount" => {
if let Ok(v) = serde_json::from_value(value) { self.budget_amount = v; }
}
"sort_order" => {
if let Ok(v) = serde_json::from_value(value) { self.sort_order = v; }
}
"show_in_reports" => {
if let Ok(v) = serde_json::from_value(value) { self.show_in_reports = v; }
}
"status" => {
if let Ok(v) = serde_json::from_value(value) { self.status = v; }
}
"is_system" => {
if let Ok(v) = serde_json::from_value(value) { self.is_system = v; }
}
"notes" => {
if let Ok(v) = serde_json::from_value(value) { self.notes = v; }
}
"source_id" => {
if let Ok(v) = serde_json::from_value(value) { self.source_id = v; }
}
"is_cloned" => {
if let Ok(v) = serde_json::from_value(value) { self.is_cloned = v; }
}
"chart_code" => {
if let Ok(v) = serde_json::from_value(value) { self.chart_code = v; }
}
"chart_version" => {
if let Ok(v) = serde_json::from_value(value) { self.chart_version = v; }
}
_ => {} }
}
}
}
impl super::Entity for Account {
type Id = Uuid;
fn entity_id(&self) -> &Self::Id {
&self.id
}
fn entity_type() -> &'static str {
"Account"
}
}
impl backbone_core::PersistentEntity for Account {
fn entity_id(&self) -> String {
self.id.to_string()
}
fn set_entity_id(&mut self, id: String) {
if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
self.id = uuid;
}
}
fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
self.metadata.created_at
}
fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
self.metadata.created_at = Some(ts);
}
fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
self.metadata.updated_at
}
fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
self.metadata.updated_at = Some(ts);
}
fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
self.metadata.deleted_at
}
fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
self.metadata.deleted_at = ts;
}
}
impl backbone_orm::EntityRepoMeta for Account {
fn column_types() -> std::collections::HashMap<String, String> {
let mut m = std::collections::HashMap::new();
m.insert("id".to_string(), "uuid".to_string());
m.insert("parent_id".to_string(), "uuid".to_string());
m.insert("source_id".to_string(), "uuid".to_string());
m.insert("account_type".to_string(), "account_type".to_string());
m.insert("account_subtype".to_string(), "account_subtype".to_string());
m.insert("normal_balance".to_string(), "normal_balance".to_string());
m.insert("status".to_string(), "account_status".to_string());
m.insert("opening_balance_date".to_string(), "date".to_string());
m
}
fn search_fields() -> &'static [&'static str] {
&["account_number", "account_code", "name", "currency"]
}
fn relations() -> &'static [(&'static str, &'static str, &'static str)] {
&[("parent", "accounts", "parentId"), ("source", "accounts", "sourceId")]
}
}
#[derive(Debug, Clone, Default)]
pub struct AccountBuilder {
account_number: Option<String>,
account_code: Option<String>,
name: Option<String>,
description: Option<String>,
account_type: Option<AccountType>,
account_subtype: Option<AccountSubtype>,
normal_balance: Option<NormalBalance>,
parent_id: Option<Uuid>,
level: Option<i32>,
path: Option<String>,
is_header: Option<bool>,
is_detail: Option<bool>,
currency: Option<String>,
opening_balance: Option<Decimal>,
opening_balance_date: Option<NaiveDate>,
current_balance: Option<Decimal>,
is_reconcilable: Option<bool>,
has_budget: Option<bool>,
budget_amount: Option<Decimal>,
sort_order: Option<i32>,
show_in_reports: Option<bool>,
status: Option<AccountStatus>,
is_system: Option<bool>,
notes: Option<String>,
source_id: Option<Uuid>,
is_cloned: Option<bool>,
chart_code: Option<String>,
chart_version: Option<String>,
}
impl AccountBuilder {
pub fn account_number(mut self, value: String) -> Self {
self.account_number = Some(value);
self
}
pub fn account_code(mut self, value: String) -> Self {
self.account_code = Some(value);
self
}
pub fn name(mut self, value: String) -> Self {
self.name = Some(value);
self
}
pub fn description(mut self, value: String) -> Self {
self.description = Some(value);
self
}
pub fn account_type(mut self, value: AccountType) -> Self {
self.account_type = Some(value);
self
}
pub fn account_subtype(mut self, value: AccountSubtype) -> Self {
self.account_subtype = Some(value);
self
}
pub fn normal_balance(mut self, value: NormalBalance) -> Self {
self.normal_balance = Some(value);
self
}
pub fn parent_id(mut self, value: Uuid) -> Self {
self.parent_id = Some(value);
self
}
pub fn level(mut self, value: i32) -> Self {
self.level = Some(value);
self
}
pub fn path(mut self, value: String) -> Self {
self.path = Some(value);
self
}
pub fn is_header(mut self, value: bool) -> Self {
self.is_header = Some(value);
self
}
pub fn is_detail(mut self, value: bool) -> Self {
self.is_detail = Some(value);
self
}
pub fn currency(mut self, value: String) -> Self {
self.currency = Some(value);
self
}
pub fn opening_balance(mut self, value: Decimal) -> Self {
self.opening_balance = Some(value);
self
}
pub fn opening_balance_date(mut self, value: NaiveDate) -> Self {
self.opening_balance_date = Some(value);
self
}
pub fn current_balance(mut self, value: Decimal) -> Self {
self.current_balance = Some(value);
self
}
pub fn is_reconcilable(mut self, value: bool) -> Self {
self.is_reconcilable = Some(value);
self
}
pub fn has_budget(mut self, value: bool) -> Self {
self.has_budget = Some(value);
self
}
pub fn budget_amount(mut self, value: Decimal) -> Self {
self.budget_amount = Some(value);
self
}
pub fn sort_order(mut self, value: i32) -> Self {
self.sort_order = Some(value);
self
}
pub fn show_in_reports(mut self, value: bool) -> Self {
self.show_in_reports = Some(value);
self
}
pub fn status(mut self, value: AccountStatus) -> Self {
self.status = Some(value);
self
}
pub fn is_system(mut self, value: bool) -> Self {
self.is_system = Some(value);
self
}
pub fn notes(mut self, value: String) -> Self {
self.notes = Some(value);
self
}
pub fn source_id(mut self, value: Uuid) -> Self {
self.source_id = Some(value);
self
}
pub fn is_cloned(mut self, value: bool) -> Self {
self.is_cloned = Some(value);
self
}
pub fn chart_code(mut self, value: String) -> Self {
self.chart_code = Some(value);
self
}
pub fn chart_version(mut self, value: String) -> Self {
self.chart_version = Some(value);
self
}
pub fn build(self) -> Result<Account, String> {
let account_number = self.account_number.ok_or_else(|| "account_number is required".to_string())?;
let account_code = self.account_code.ok_or_else(|| "account_code is required".to_string())?;
let name = self.name.ok_or_else(|| "name is required".to_string())?;
let account_type = self.account_type.ok_or_else(|| "account_type is required".to_string())?;
let account_subtype = self.account_subtype.ok_or_else(|| "account_subtype is required".to_string())?;
let normal_balance = self.normal_balance.ok_or_else(|| "normal_balance is required".to_string())?;
Ok(Account {
id: Uuid::new_v4(),
account_number,
account_code,
name,
description: self.description,
account_type,
account_subtype,
normal_balance,
parent_id: self.parent_id,
level: self.level.unwrap_or(0),
path: self.path,
is_header: self.is_header.unwrap_or(false),
is_detail: self.is_detail.unwrap_or(true),
currency: self.currency.unwrap_or("IDR".to_string()),
opening_balance: self.opening_balance.unwrap_or(Decimal::from(0)),
opening_balance_date: self.opening_balance_date,
current_balance: self.current_balance.unwrap_or(Decimal::from(0)),
is_reconcilable: self.is_reconcilable.unwrap_or(false),
has_budget: self.has_budget.unwrap_or(false),
budget_amount: self.budget_amount,
sort_order: self.sort_order.unwrap_or(0),
show_in_reports: self.show_in_reports.unwrap_or(true),
status: self.status.unwrap_or_default(),
is_system: self.is_system.unwrap_or(false),
notes: self.notes,
source_id: self.source_id,
is_cloned: self.is_cloned.unwrap_or(false),
chart_code: self.chart_code,
chart_version: self.chart_version,
metadata: AuditMetadata::default(),
})
}
}