use chrono::NaiveDate;
use rust_decimal::Decimal;
use sqlx::PgPool;
use std::collections::HashMap;
use crate::application::service::statutory_calcs::{
BpjsConfig, BpjsKesehatanConfig, BpjsTkConfig, OvertimeBand, OvertimeConfig, Pph21Bracket,
Pph21Config, StatutoryConfig, StatutoryError, TerRateBand,
};
pub struct StatutoryParamsRepository {
pool: PgPool,
}
impl StatutoryParamsRepository {
fn rpool(&self) -> PgPool {
crate::request_pool::current().unwrap_or_else(|| self.pool.clone())
}
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
pub async fn resolve_as_of(
&self,
country_code: &str,
as_of: NaiveDate,
) -> Result<StatutoryConfig, StatutoryError> {
let brackets = self.brackets_as_of(country_code, as_of).await?;
let ptkp_map = self.ptkp_as_of(country_code, as_of).await?;
let ter = self.ter_as_of(country_code, as_of).await?;
let bpjs = self.bpjs_as_of(country_code, as_of).await?;
let overtime = self.overtime_as_of(country_code, as_of).await?;
Ok(StatutoryConfig {
pph21: Pph21Config {
brackets,
ptkp_map,
npwp_surtax_multiplier: Decimal::new(12, 1),
ter,
},
bpjs,
overtime,
})
}
async fn effective_at(
&self,
table: &str,
country_code: &str,
as_of: NaiveDate,
) -> Result<Option<NaiveDate>, sqlx::Error> {
let effective: Option<(Option<NaiveDate>,)> = sqlx::query_as(&format!(
"SELECT MAX(effective_from) FROM {table} WHERE country_code = $1 AND effective_from <= $2"
))
.bind(country_code)
.bind(as_of)
.fetch_optional(&self.rpool())
.await?;
Ok(effective.and_then(|(d,)| d))
}
async fn brackets_as_of(
&self,
country_code: &str,
as_of: NaiveDate,
) -> Result<Vec<Pph21Bracket>, StatutoryError> {
let effective = self
.effective_at("payroll.pph21_brackets", country_code, as_of)
.await?
.ok_or_else(|| no_params(country_code, as_of))?;
let rows: Vec<(i32, Decimal, Option<Decimal>, Decimal)> = sqlx::query_as(
r#"SELECT seq, lower_bound, upper_bound, rate
FROM payroll.pph21_brackets
WHERE country_code = $1 AND effective_from = $2
ORDER BY seq"#,
)
.bind(country_code)
.bind(effective)
.fetch_all(&self.rpool())
.await?;
let brackets: Vec<Pph21Bracket> = rows
.into_iter()
.map(|(_, lower, upper, rate)| Pph21Bracket { lower_bound: lower, upper_bound: upper, rate })
.collect();
if brackets.is_empty()
|| brackets.first().map(|b| b.lower_bound) != Some(Decimal::ZERO)
|| brackets.last().and_then(|b| b.upper_bound).is_some()
{
return Err(no_params(country_code, as_of));
}
Ok(brackets)
}
async fn ptkp_as_of(
&self,
country_code: &str,
as_of: NaiveDate,
) -> Result<HashMap<String, Decimal>, StatutoryError> {
let effective = self
.effective_at("payroll.pph21_ptkp", country_code, as_of)
.await?
.ok_or_else(|| no_params(country_code, as_of))?;
let rows: Vec<(String, Decimal)> = sqlx::query_as(
r#"SELECT tier, annual_amount
FROM payroll.pph21_ptkp
WHERE country_code = $1 AND effective_from = $2"#,
)
.bind(country_code)
.bind(effective)
.fetch_all(&self.rpool())
.await?;
let ptkp_map: HashMap<String, Decimal> = rows.into_iter().collect();
for tier in ["tk0", "tk1", "tk2", "tk3", "k0", "k1", "k2", "k3"] {
if !ptkp_map.contains_key(tier) {
return Err(no_params(country_code, as_of));
}
}
Ok(ptkp_map)
}
async fn ter_as_of(
&self,
country_code: &str,
as_of: NaiveDate,
) -> Result<HashMap<String, Vec<TerRateBand>>, StatutoryError> {
let effective = self
.effective_at("payroll.pph21_ter_rates", country_code, as_of)
.await?
.ok_or_else(|| no_params(country_code, as_of))?;
let rows: Vec<(String, i32, Decimal, Decimal)> = sqlx::query_as(
r#"SELECT category, seq, lower_bound, rate
FROM payroll.pph21_ter_rates
WHERE country_code = $1 AND effective_from = $2
ORDER BY category, seq"#,
)
.bind(country_code)
.bind(effective)
.fetch_all(&self.rpool())
.await?;
let mut map: HashMap<String, Vec<TerRateBand>> = HashMap::new();
for (category, _, lower, rate) in rows {
map.entry(category).or_default().push(TerRateBand { lower_bound: lower, rate });
}
for category in ["ter_a", "ter_b", "ter_c"] {
match map.get(category) {
Some(bands)
if !bands.is_empty() && bands.first().map(|b| b.lower_bound) == Some(Decimal::ZERO) =>
{}
_ => return Err(no_params(country_code, as_of)),
}
}
Ok(map)
}
async fn bpjs_as_of(&self, country_code: &str, as_of: NaiveDate) -> Result<BpjsConfig, StatutoryError> {
let effective = self
.effective_at("payroll.bpjs_params", country_code, as_of)
.await?
.ok_or_else(|| no_params(country_code, as_of))?;
let rows: Vec<(String, String, Decimal, Option<Decimal>)> = sqlx::query_as(
r#"SELECT component, side, rate, wage_cap
FROM payroll.bpjs_params
WHERE country_code = $1 AND effective_from = $2"#,
)
.bind(country_code)
.bind(effective)
.fetch_all(&self.rpool())
.await?;
let mut kes_employee = None;
let mut kes_employer = None;
let mut kes_cap: Option<Option<Decimal>> = None;
let mut jht_employee = None;
let mut jht_employer = None;
let mut jp_employee = None;
let mut jp_employer = None;
let mut jp_cap: Option<Option<Decimal>> = None;
let mut jkk: HashMap<String, Decimal> = HashMap::new();
let mut jkm = None;
for (component, side, rate, cap) in rows {
match (component.as_str(), side.as_str()) {
("kes", "employee") => kes_employee = Some(rate),
("kes", "employer") => kes_employer = Some(rate),
("jht", "employee") => jht_employee = Some(rate),
("jht", "employer") => jht_employer = Some(rate),
("jp", "employee") => jp_employee = Some(rate),
("jp", "employer") => jp_employer = Some(rate),
(jk, "employer") if jk.starts_with("jkk_") => {
jkk.insert(jk.trim_start_matches("jkk_").to_string(), rate);
}
("jkm", "employer") => jkm = Some(rate),
_ => return Err(no_params(country_code, as_of)),
}
if component == "kes" {
kes_cap = Some(cap);
}
if component == "jp" {
jp_cap = Some(cap);
}
}
Ok(BpjsConfig {
kesehatan: BpjsKesehatanConfig {
employee_rate: kes_employee.ok_or_else(|| no_params(country_code, as_of))?,
employer_rate: kes_employer.ok_or_else(|| no_params(country_code, as_of))?,
salary_cap: kes_cap.flatten().ok_or_else(|| no_params(country_code, as_of))?,
},
ketenagakerjaan: BpjsTkConfig {
jht_employee_rate: jht_employee.ok_or_else(|| no_params(country_code, as_of))?,
jht_employer_rate: jht_employer.ok_or_else(|| no_params(country_code, as_of))?,
jp_employee_rate: jp_employee.ok_or_else(|| no_params(country_code, as_of))?,
jp_employer_rate: jp_employer.ok_or_else(|| no_params(country_code, as_of))?,
jp_salary_cap: jp_cap.flatten().ok_or_else(|| no_params(country_code, as_of))?,
jkk_rates_by_risk_class: jkk,
jkm_rate: jkm.ok_or_else(|| no_params(country_code, as_of))?,
},
})
}
async fn overtime_as_of(
&self,
country_code: &str,
as_of: NaiveDate,
) -> Result<OvertimeConfig, StatutoryError> {
let effective = self
.effective_at("payroll.overtime_params", country_code, as_of)
.await?
.ok_or_else(|| no_params(country_code, as_of))?;
let rows: Vec<(String, i32, Option<i32>, Decimal)> = sqlx::query_as(
r#"SELECT day_kind, hour_from, hour_to, multiplier
FROM payroll.overtime_params
WHERE country_code = $1 AND effective_from = $2
ORDER BY day_kind, hour_from"#,
)
.bind(country_code)
.bind(effective)
.fetch_all(&self.rpool())
.await?;
let mut workday = Vec::new();
let mut rest_day = Vec::new();
for (day_kind, hour_from, hour_to, multiplier) in rows {
let band = OvertimeBand { hour_from, hour_to, multiplier };
match day_kind.as_str() {
"workday" => workday.push(band),
"rest_day" => rest_day.push(band),
_ => return Err(no_params(country_code, as_of)),
}
}
if workday.is_empty() {
return Err(no_params(country_code, as_of));
}
Ok(OvertimeConfig {
hours_per_month: Decimal::new(173, 0),
workday,
rest_day,
})
}
}
fn no_params(country_code: &str, as_of: NaiveDate) -> StatutoryError {
StatutoryError::NoParamsForPeriod(country_code.to_string(), as_of.to_string())
}