use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::post,
Json, Router,
};
use backbone_auth::org::OrgContext;
use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::application::service::{
ComputedSlipRequest, NewComponent, NewPayrollEntry, NewStructure, PayrollError,
PayrollWriteService, StatutoryAccounts,
};
use crate::presentation::http::{
create_compensation_change_read_routes, create_payroll_entry_read_routes,
create_salary_component_routes, create_salary_slip_line_read_routes,
create_salary_slip_read_routes, create_salary_structure_routes,
};
use crate::PayrollModule;
#[derive(Debug, Serialize)]
struct ErrorBody {
error: &'static str,
message: String,
}
fn err_response(e: PayrollError) -> axum::response::Response {
let status = StatusCode::from_u16(e.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
(
status,
Json(ErrorBody { error: e.code(), message: e.to_string() }),
)
.into_response()
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateStructureBody {
name: String,
components: Vec<ComponentBody>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ComponentBody {
name: String,
component_type: String, amount: Decimal,
gl_account_id: Uuid,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateRunBody {
period_year: i32,
period_month: i32,
#[serde(default)]
period_start: Option<chrono::NaiveDate>,
#[serde(default)]
period_end: Option<chrono::NaiveDate>,
salary_expense_account_id: Uuid,
salary_payable_account_id: Uuid,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ComputedSlipBody {
employee_id: Uuid,
structure_id: Uuid,
working_days: Decimal,
#[serde(default)]
unpaid_days: Decimal,
#[serde(default = "default_risk_class")]
risk_class: i32,
statutory_accounts: StatutoryAccountsBody,
}
fn default_risk_class() -> i32 {
3
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct StatutoryAccountsBody {
pph21_payable_account_id: Uuid,
bpjs_kesehatan_payable_account_id: Uuid,
bpjs_ketenagakerjaan_payable_account_id: Uuid,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PostBody {
#[serde(default = "default_posting_date")]
posting_date: NaiveDate,
}
fn default_posting_date() -> NaiveDate {
chrono::Utc::now().date_naive()
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct IdBody {
id: Uuid,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct PostOutcomeBody {
payroll_entry_id: Uuid,
journal_id: Uuid,
post_id: Uuid,
total_net: Decimal,
already: bool,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RemitAckBody {
gl_account_id: Uuid,
amount: Decimal,
statutory: bool,
idempotency_key: String,
duplicate: bool,
payment_id: Option<Uuid>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RemitOutcomeBody {
payroll_entry_id: Uuid,
remitted: Vec<RemitAckBody>,
}
async fn create_structure(
State(svc): State<Arc<PayrollWriteService>>,
_org: OrgContext,
Json(b): Json<CreateStructureBody>,
) -> axum::response::Response {
match svc
.create_structure(NewStructure {
name: b.name,
components: b
.components
.into_iter()
.map(|c| NewComponent {
name: c.name,
component_type: c.component_type,
amount: c.amount,
gl_account_id: c.gl_account_id,
})
.collect(),
})
.await
{
Ok(id) => (StatusCode::CREATED, Json(IdBody { id })).into_response(),
Err(e) => err_response(e),
}
}
async fn create_run(
State(svc): State<Arc<PayrollWriteService>>,
_org: OrgContext,
Json(b): Json<CreateRunBody>,
) -> axum::response::Response {
match svc
.create_payroll_entry(NewPayrollEntry {
period_year: b.period_year,
period_month: b.period_month,
period_start: b.period_start,
period_end: b.period_end,
salary_expense_account_id: b.salary_expense_account_id,
salary_payable_account_id: b.salary_payable_account_id,
})
.await
{
Ok(id) => (StatusCode::CREATED, Json(IdBody { id })).into_response(),
Err(e) => err_response(e),
}
}
async fn add_computed_slip(
State(svc): State<Arc<PayrollWriteService>>,
_org: OrgContext,
Path(run_id): Path<Uuid>,
Json(b): Json<ComputedSlipBody>,
) -> axum::response::Response {
match svc
.add_computed_salary_slip(ComputedSlipRequest {
run_id,
employee_id: b.employee_id,
structure_id: b.structure_id,
working_days: b.working_days,
unpaid_days: b.unpaid_days,
risk_class: b.risk_class,
accounts: StatutoryAccounts {
pph21_payable: b.statutory_accounts.pph21_payable_account_id,
bpjs_kesehatan_payable: b.statutory_accounts.bpjs_kesehatan_payable_account_id,
bpjs_ketenagakerjaan_payable: b.statutory_accounts.bpjs_ketenagakerjaan_payable_account_id,
},
})
.await
{
Ok(id) => (StatusCode::CREATED, Json(IdBody { id })).into_response(),
Err(e) => err_response(e),
}
}
async fn process_run(
State(svc): State<Arc<PayrollWriteService>>,
_org: OrgContext,
Path(run_id): Path<Uuid>,
) -> axum::response::Response {
match svc.process_payroll_entry(run_id).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => err_response(e),
}
}
async fn cancel_run(
State(svc): State<Arc<PayrollWriteService>>,
_org: OrgContext,
Path(run_id): Path<Uuid>,
) -> axum::response::Response {
match svc.cancel_payroll_entry(run_id).await {
Ok(true) => (StatusCode::OK, Json(serde_json::json!({ "status": "cancelled" }))).into_response(),
Ok(false) => (StatusCode::OK, Json(serde_json::json!({ "status": "cancelled", "already": true }))).into_response(),
Err(e) => err_response(e),
}
}
async fn post_run(
State(svc): State<Arc<PayrollWriteService>>,
_org: OrgContext,
Path(run_id): Path<Uuid>,
body: Option<Json<PostBody>>,
) -> axum::response::Response {
let posting_date = body.map(|Json(b)| b.posting_date).unwrap_or_else(default_posting_date);
match svc.post_run(run_id, posting_date).await {
Ok(o) => (
StatusCode::OK,
Json(PostOutcomeBody {
payroll_entry_id: o.payroll_entry_id,
journal_id: o.journal_id,
post_id: o.post_id,
total_net: o.total_net,
already: o.already,
}),
)
.into_response(),
Err(e) => err_response(e),
}
}
async fn remit_run(
State(svc): State<Arc<PayrollWriteService>>,
_org: OrgContext,
Path(run_id): Path<Uuid>,
) -> axum::response::Response {
match svc.remit_run(run_id).await {
Ok(o) => (
StatusCode::OK,
Json(RemitOutcomeBody {
payroll_entry_id: o.payroll_entry_id,
remitted: o
.remitted
.into_iter()
.map(|(i, a)| RemitAckBody {
gl_account_id: i.gl_account_id,
amount: i.amount,
statutory: i.statutory,
idempotency_key: i.idempotency_key,
duplicate: a.duplicate,
payment_id: a.payment_id,
})
.collect(),
}),
)
.into_response(),
Err(e) => err_response(e),
}
}
pub fn create_guarded_payroll_routes(m: &PayrollModule) -> Router {
let writes = Router::new()
.route("/salary-structures", post(create_structure))
.route("/payroll-entries", post(create_run))
.route("/payroll-entries/:id/slips", post(add_computed_slip))
.route("/payroll-entries/:id/process", post(process_run))
.route("/payroll-entries/:id/post", post(post_run))
.route("/payroll-entries/:id/cancel", post(cancel_run))
.route("/payroll-entries/:id/remit", post(remit_run))
.with_state(m.payroll_write_service());
Router::new()
.merge(create_payroll_entry_read_routes(m.payroll_entry_service.clone()))
.merge(create_salary_slip_read_routes(m.salary_slip_service.clone()))
.merge(create_salary_slip_line_read_routes(m.salary_slip_line_service.clone()))
.merge(create_compensation_change_read_routes(m.compensation_change_service.clone()))
.merge(create_salary_structure_routes(m.salary_structure_service.clone()))
.merge(create_salary_component_routes(m.salary_component_service.clone()))
.merge(writes)
.layer(axum::middleware::from_fn(
crate::request_pool::bind_request_pool,
))
}