backbone-payroll 0.3.47

Payroll: salary structures, payroll runs and computed salary slips over effective-dated statutory tables, plus compensation changes
Documentation
//! Payroll Module
//!
//! Generated by metaphor-schema. Enhanced with runtime implementations.
//!
//! This module provides:
//! - Domain entities and repositories
//! - Application services
//! - HTTP and gRPC handlers
//! - Route configuration
//! - State machine enforcement
//! - Validation rules runtime
//! - RBAC middleware
//! - Trigger execution system
//! - Computed fields
//! - Workflow orchestrator

#![recursion_limit = "1024"]
#![allow(unused_imports)]

// Generated modules
pub mod domain;
pub mod infrastructure;
pub mod application;
pub mod presentation;
pub mod seeders;
pub mod exports;

// Re-exports for convenience - Domain entities
pub use domain::entity::*;

// Re-exports - Infrastructure
pub use infrastructure::persistence::*;

// Re-exports - Application services
pub use application::service::CompensationChangeService;
pub use application::service::PayrollEntryService;
pub use application::service::SalarySlipService;
pub use application::service::SalarySlipLineService;
pub use application::service::SalaryStructureService;
pub use application::service::SalaryComponentService;

// <<< CUSTOM
// The hand-owned request-pool shim (the composing service's tenant pool
// resolution): a generated-tree declaration the regenerator drops, so it
// lives in the preserved block (#447 cause-2 class).
pub mod request_pool;
// The validated run verbs (lifecycle + computed slips + the write-service request/outcome types),
// the fail-closed seams (GL post / remittance / domain events), the overtime + employee-statutory
// input ports, the statutory calculators, and the guarded HTTP composition. Re-exported so a
// host composes payroll without reaching into internal module paths.
pub use application::service::{
    bpjs_kesehatan, bpjs_ketenagakerjaan, compute_statutory, overtime_pay, pph21, pph21_ter, thr,
    AccountingPostEnvelope, ComputedSlipRequest, EmployeeStatutory, EmployeeStatutoryInputs,
    GlPostAck, GlPostLine, GlPostRejected, GlPostSink, LoggingSink, NewComponent, NewPayrollEntry,
    NewSalarySlip, NewStructure, OvertimeInputs, PayrollError, PayrollEvent, PayrollEventError,
    PayrollEventSink, PayrollPayable, PayrollPosted, PayrollWriteService, PoolEmployeeStatutoryInputs,
    PoolOvertimeInputs, PostOutcome, Pph21Method, RemitAck, RemitOutcome, RemittanceInstruction,
    RemittanceSeamError, RemittanceSink, StatutoryAccounts, StatutoryConfig, StatutoryError,
    UnwiredGlSink, UnwiredRemittance,
};
pub use presentation::http::create_guarded_payroll_routes;
// END CUSTOM
use std::sync::Arc;
use axum::Router;
use sqlx::PgPool;

/// Payroll module configuration
///
/// Use the builder pattern to configure and register this module:
///
/// ```text
/// let payroll = PayrollModule::builder()
///     .with_database(pool.clone())
///     .build()?;
///
/// // Unguarded full CRUD (trusted/admin); compose a guarded router for production.
/// let router = payroll.all_crud_routes();
/// ```
pub struct PayrollModule {
    pub(crate) compensation_change_service: Arc<CompensationChangeService>,
    pub(crate) payroll_entry_service: Arc<PayrollEntryService>,
    pub(crate) salary_slip_service: Arc<SalarySlipService>,
    pub(crate) salary_slip_line_service: Arc<SalarySlipLineService>,
    pub(crate) salary_structure_service: Arc<SalaryStructureService>,
    pub(crate) salary_component_service: Arc<SalaryComponentService>,
    // <<< CUSTOM FIELDS
    // The validated write path: run lifecycle, computed slips, the balanced salary journal, and
    // the module-held fail-closed seams (GL / events / remittance default Unwired/Logging/Unwired).
    pub(crate) payroll_write_service: Arc<PayrollWriteService>,
    // END CUSTOM
}

impl PayrollModule {
    /// Create a new module builder
    pub fn builder() -> PayrollModuleBuilder {
        PayrollModuleBuilder::new()
    }

    /// Mount ALL generated CRUD endpoints (12 per entity) with NO domain
    /// validation — the fully **unguarded** surface. A well-formed request can
    /// create invalid rows or soft-delete a referenced master out from under its
    /// dependents. Prefer a guarded composition (read + validated writes) for any
    /// real deployment; use this only in trusted/admin/seeding contexts.
    pub fn all_crud_routes(&self) -> Router {
        use presentation::http::{
            create_compensation_change_read_routes,
            create_payroll_entry_routes,
            create_salary_slip_routes,
            create_salary_slip_line_routes,
            create_salary_structure_routes,
            create_salary_component_routes,
        };

        Router::new()
            .merge(create_compensation_change_read_routes(self.compensation_change_service.clone()))
            .merge(create_payroll_entry_routes(self.payroll_entry_service.clone()))
            .merge(create_salary_slip_routes(self.salary_slip_service.clone()))
            .merge(create_salary_slip_line_routes(self.salary_slip_line_service.clone()))
            .merge(create_salary_structure_routes(self.salary_structure_service.clone()))
            .merge(create_salary_component_routes(self.salary_component_service.clone()))
    }

    /// Deprecated alias for [`Self::all_crud_routes`]. `routes()` reads like
    /// "the routes" but mounts UNVALIDATED generic CRUD on every entity — a naive
    /// mount exposes unguarded writes. Compose a guarded router (read + validated
    /// writes) for production, or call `all_crud_routes()` to opt into the full
    /// unguarded surface explicitly.
    #[deprecated(note = "mounts unvalidated generic CRUD; prefer readonly_routes() + validated writes, or all_crud_routes() for the full/unguarded surface")]
    pub fn routes(&self) -> Router {
        self.all_crud_routes()
    }

    /// Read-only routes for every entity (GET endpoints only) — the safe base.
    ///
    /// Generic mutation can't reach here, so this surface cannot bypass a
    /// validated write service's invariants. Use this as the production base and
    /// merge validated write routes (or a write service's HTTP layer) onto it.
    pub fn readonly_routes(&self) -> Router {
        use presentation::http::{
            create_compensation_change_read_routes,
            create_payroll_entry_read_routes,
            create_salary_slip_read_routes,
            create_salary_slip_line_read_routes,
            create_salary_structure_read_routes,
            create_salary_component_read_routes,
        };

        Router::new()
            .merge(create_compensation_change_read_routes(self.compensation_change_service.clone()))
            .merge(create_payroll_entry_read_routes(self.payroll_entry_service.clone()))
            .merge(create_salary_slip_read_routes(self.salary_slip_service.clone()))
            .merge(create_salary_slip_line_read_routes(self.salary_slip_line_service.clone()))
            .merge(create_salary_structure_read_routes(self.salary_structure_service.clone()))
            .merge(create_salary_component_read_routes(self.salary_component_service.clone()))
    }

    // <<< CUSTOM METHODS
    /// The module-held validated write service (run verbs, computed slips, seams).
    pub fn payroll_write_service(&self) -> Arc<PayrollWriteService> {
        self.payroll_write_service.clone()
    }

    /// Replace the write service at the composition root — the way a host wires real adapters over
    /// the fail-closed seams (e.g. staging domain events into an outbox) or swaps the pool-default
    /// input ports for adapters over the attendance/employee exports. Consuming `self` keeps the
    /// builder chain reading naturally:
    ///
    /// ```text
    /// let payroll = PayrollModule::builder()
    ///     .with_database(pool.clone())
    ///     .build()?
    ///     .with_payroll_write_service(Arc::new(
    ///         PayrollWriteService::new(pool.clone()).with_event_sink(my_outbox_sink),
    ///     ));
    /// ```
    ///
    /// Call before mounting routes; the module is consumed once at startup.
    pub fn with_payroll_write_service(mut self, svc: Arc<PayrollWriteService>) -> Self {
        self.payroll_write_service = svc;
        self
    }
    // END CUSTOM
}

/// Builder for PayrollModule
pub struct PayrollModuleBuilder {
    db_pool: Option<PgPool>,
}

impl PayrollModuleBuilder {
    /// Create a new builder
    pub fn new() -> Self {
        Self {
            db_pool: None,
        }
    }

    /// Set the database connection pool
    pub fn with_database(mut self, pool: PgPool) -> Self {
        self.db_pool = Some(pool);
        self
    }

    // <<< CUSTOM - custom builder methods
    // END CUSTOM

    /// Build the module with configured dependencies
    pub fn build(self) -> anyhow::Result<PayrollModule> {
        let db_pool = self.db_pool
            .ok_or_else(|| anyhow::anyhow!("Database pool not configured"))?;

        // CompensationChange service
        let compensation_change_repository = Arc::new(CompensationChangeRepository::new(db_pool.clone()));
        let compensation_change_service = Arc::new(CompensationChangeService::with_repository(compensation_change_repository.clone()));

        // PayrollEntry service
        let payroll_entry_repository = Arc::new(PayrollEntryRepository::new(db_pool.clone()));
        let payroll_entry_service = Arc::new(PayrollEntryService::with_repository(payroll_entry_repository.clone()));

        // SalarySlip service
        let salary_slip_repository = Arc::new(SalarySlipRepository::new(db_pool.clone()));
        let salary_slip_service = Arc::new(SalarySlipService::with_repository(salary_slip_repository.clone()));

        // SalarySlipLine service
        let salary_slip_line_repository = Arc::new(SalarySlipLineRepository::new(db_pool.clone()));
        let salary_slip_line_service = Arc::new(SalarySlipLineService::with_repository(salary_slip_line_repository.clone()));

        // SalaryStructure service
        let salary_structure_repository = Arc::new(SalaryStructureRepository::new(db_pool.clone()));
        let salary_structure_service = Arc::new(SalaryStructureService::with_repository(salary_structure_repository.clone()));

        // SalaryComponent service
        let salary_component_repository = Arc::new(SalaryComponentRepository::new(db_pool.clone()));
        let salary_component_service = Arc::new(SalaryComponentService::with_repository(salary_component_repository.clone()));

        // <<< CUSTOM
        // The validated write path over the same pool: pool-default input ports (works standalone),
        // seams fail-closed (Unwired GL / Logging events / Unwired remittance) — a composition root
        // swaps them via `with_payroll_write_service`.
        let payroll_write_service = Arc::new(PayrollWriteService::new(db_pool.clone()));
        // END CUSTOM

        Ok(PayrollModule {
            compensation_change_service,
            payroll_entry_service,
            salary_slip_service,
            salary_slip_line_service,
            salary_structure_service,
            salary_component_service,
            // <<< CUSTOM
            payroll_write_service,
            // END CUSTOM
        })
    }
}

impl Default for PayrollModuleBuilder {
    fn default() -> Self {
        Self::new()
    }
}