#![allow(dead_code)]
use std::sync::{Arc, Mutex};
use backbone_accounting::application::service::posting_service::{
PostingLine, PostingRequest, PostingService,
};
use backbone_accounting::infrastructure::persistence::SqlxPostingRepository;
use backbone_payroll::application::service::payroll_events::{
PayrollEvent, PayrollEventError, PayrollEventSink,
};
use backbone_payroll::application::service::payroll_gl::{
AccountingPostEnvelope, GlPostAck, GlPostRejected, GlPostSink,
};
use rust_decimal::Decimal;
use sqlx::PgPool;
use uuid::Uuid;
pub fn dburl() -> String {
std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5433/backbone_payroll".into())
}
pub async fn pool() -> PgPool {
PgPool::connect(&dburl()).await.expect("connect")
}
pub fn dec(s: &str) -> Decimal {
s.parse().unwrap()
}
pub fn today() -> chrono::NaiveDate {
chrono::Utc::now().date_naive()
}
pub fn npwp() -> String {
let u = Uuid::new_v4().as_u128();
let n = (u % 1_000_000_000_000_000) as u64;
format!("{n:015}")
}
pub async fn account(pool: &PgPool, code: &str, atype: &str, subtype: &str, normal: &str) -> Uuid {
let id = Uuid::new_v4();
sqlx::query(
r#"INSERT INTO accounting.accounts
(id, account_number, account_code, name, account_type, account_subtype,
normal_balance, is_header, is_detail, status)
VALUES ($1,$2,$3,$4,$5::account_type,$6::account_subtype,$7::normal_balance,
false,true,'active'::account_status)"#,
)
.bind(id).bind(code).bind(code).bind(code).bind(atype).bind(subtype).bind(normal)
.execute(pool).await.expect("seed account");
id
}
pub async fn balance(pool: &PgPool, account: Uuid) -> Decimal {
sqlx::query_scalar(
"SELECT COALESCE(SUM(debit_amount),0) - COALESCE(SUM(credit_amount),0)
FROM accounting.ledgers WHERE account_id=$1",
)
.bind(account)
.fetch_one(pool)
.await
.expect("balance")
}
pub struct PayrollAccounts {
pub salary_expense: Uuid,
pub salary_payable: Uuid,
pub bpjs_payable: Uuid,
pub pph21_payable: Uuid,
}
pub async fn payroll_accounts(pool: &PgPool) -> PayrollAccounts {
PayrollAccounts {
salary_expense: account(pool, "6100-SAL", "expense", "operating_expense", "debit").await,
salary_payable: account(pool, "2100-SPY", "liability", "current_liability", "credit").await,
bpjs_payable: account(pool, "2110-BPJS", "liability", "current_liability", "credit").await,
pph21_payable: account(pool, "2120-PPH", "liability", "current_liability", "credit").await,
}
}
pub struct GlAdapter {
pub svc: PostingService,
}
impl GlAdapter {
pub fn new(pool: PgPool) -> Self {
Self { svc: PostingService::new(Arc::new(SqlxPostingRepository::new(pool))) }
}
}
#[async_trait::async_trait]
impl GlPostSink for GlAdapter {
async fn post(&self, e: &AccountingPostEnvelope) -> Result<GlPostAck, GlPostRejected> {
let mut r = PostingRequest::original(e.company_id, &e.source_type, e.source_id, e.posting_date);
r.source_reference = e.source_reference.clone();
r.posting_type = e.posting_type.clone();
r.lines = e.lines.iter().map(|l| PostingLine {
account_id: l.account_id, debit: l.debit, credit: l.credit,
party_type: l.party_type.clone(), party_id: l.party_id,
cost_center_id: None, project_id: None, department_id: None, description: l.description.clone(),
}).collect();
match self.svc.post(r, None).await {
Ok(x) => Ok(GlPostAck { post_id: x.post_id, journal_id: x.journal_id, idempotent_reuse: x.idempotent_reuse }),
Err(x) => Err(GlPostRejected { code: x.code().to_string(), message: x.to_string() }),
}
}
}
#[derive(Clone, Default)]
pub struct CountingGl {
pub posts: Arc<Mutex<Vec<AccountingPostEnvelope>>>,
}
impl CountingGl {
pub fn new() -> Self {
Self::default()
}
pub fn count(&self) -> usize {
self.posts.lock().unwrap().len()
}
pub fn last(&self) -> AccountingPostEnvelope {
self.posts.lock().unwrap().last().cloned().expect("a post")
}
}
#[async_trait::async_trait]
impl GlPostSink for CountingGl {
async fn post(&self, e: &AccountingPostEnvelope) -> Result<GlPostAck, GlPostRejected> {
self.posts.lock().unwrap().push(e.clone());
Ok(GlPostAck { post_id: Uuid::new_v4(), journal_id: Uuid::new_v4(), idempotent_reuse: false })
}
}
#[derive(Clone, Default)]
pub struct CapturingEvents {
pub events: Arc<Mutex<Vec<PayrollEvent>>>,
}
impl CapturingEvents {
pub fn new() -> Self {
Self::default()
}
pub fn last_posted(&self) -> backbone_payroll::application::service::payroll_events::PayrollPosted {
match self.events.lock().unwrap().last().cloned().expect("a PayrollPosted") {
PayrollEvent::PayrollPosted(p) => p,
}
}
}
#[async_trait::async_trait]
impl PayrollEventSink for CapturingEvents {
async fn publish(&self, event: &PayrollEvent) -> Result<(), PayrollEventError> {
self.events.lock().unwrap().push(event.clone());
Ok(())
}
}