Skip to main content

backbone_payroll/
lib.rs

1//! Payroll Module
2//!
3//! Generated by metaphor-schema. Enhanced with runtime implementations.
4//!
5//! This module provides:
6//! - Domain entities and repositories
7//! - Application services
8//! - HTTP and gRPC handlers
9//! - Route configuration
10//! - State machine enforcement
11//! - Validation rules runtime
12//! - RBAC middleware
13//! - Trigger execution system
14//! - Computed fields
15//! - Workflow orchestrator
16
17#![recursion_limit = "1024"]
18#![allow(unused_imports)]
19
20// Generated modules
21pub mod domain;
22pub mod infrastructure;
23pub mod application;
24pub mod presentation;
25pub mod seeders;
26pub mod exports;
27
28// Re-exports for convenience - Domain entities
29pub use domain::entity::*;
30
31// Re-exports - Infrastructure
32pub use infrastructure::persistence::*;
33
34// Re-exports - Application services
35pub use application::service::CompensationChangeService;
36pub use application::service::PayrollEntryService;
37pub use application::service::SalarySlipService;
38pub use application::service::SalarySlipLineService;
39pub use application::service::SalaryStructureService;
40pub use application::service::SalaryComponentService;
41
42// <<< CUSTOM
43// The hand-owned request-pool shim (the composing service's tenant pool
44// resolution): a generated-tree declaration the regenerator drops, so it
45// lives in the preserved block (#447 cause-2 class).
46pub mod request_pool;
47// The validated run verbs (lifecycle + computed slips + the write-service request/outcome types),
48// the fail-closed seams (GL post / remittance / domain events), the overtime + employee-statutory
49// input ports, the statutory calculators, and the guarded HTTP composition. Re-exported so a
50// host composes payroll without reaching into internal module paths.
51pub use application::service::{
52    bpjs_kesehatan, bpjs_ketenagakerjaan, compute_statutory, overtime_pay, pph21, pph21_ter, thr,
53    AccountingPostEnvelope, ComputedSlipRequest, EmployeeStatutory, EmployeeStatutoryInputs,
54    GlPostAck, GlPostLine, GlPostRejected, GlPostSink, LoggingSink, NewComponent, NewPayrollEntry,
55    NewSalarySlip, NewStructure, OvertimeInputs, PayrollError, PayrollEvent, PayrollEventError,
56    PayrollEventSink, PayrollPayable, PayrollPosted, PayrollWriteService, PoolEmployeeStatutoryInputs,
57    PoolOvertimeInputs, PostOutcome, Pph21Method, RemitAck, RemitOutcome, RemittanceInstruction,
58    RemittanceSeamError, RemittanceSink, StatutoryAccounts, StatutoryConfig, StatutoryError,
59    UnwiredGlSink, UnwiredRemittance,
60};
61pub use presentation::http::create_guarded_payroll_routes;
62// END CUSTOM
63use std::sync::Arc;
64use axum::Router;
65use sqlx::PgPool;
66
67/// Payroll module configuration
68///
69/// Use the builder pattern to configure and register this module:
70///
71/// ```text
72/// let payroll = PayrollModule::builder()
73///     .with_database(pool.clone())
74///     .build()?;
75///
76/// // Unguarded full CRUD (trusted/admin); compose a guarded router for production.
77/// let router = payroll.all_crud_routes();
78/// ```
79pub struct PayrollModule {
80    pub(crate) compensation_change_service: Arc<CompensationChangeService>,
81    pub(crate) payroll_entry_service: Arc<PayrollEntryService>,
82    pub(crate) salary_slip_service: Arc<SalarySlipService>,
83    pub(crate) salary_slip_line_service: Arc<SalarySlipLineService>,
84    pub(crate) salary_structure_service: Arc<SalaryStructureService>,
85    pub(crate) salary_component_service: Arc<SalaryComponentService>,
86    // <<< CUSTOM FIELDS
87    // The validated write path: run lifecycle, computed slips, the balanced salary journal, and
88    // the module-held fail-closed seams (GL / events / remittance default Unwired/Logging/Unwired).
89    pub(crate) payroll_write_service: Arc<PayrollWriteService>,
90    // END CUSTOM
91}
92
93impl PayrollModule {
94    /// Create a new module builder
95    pub fn builder() -> PayrollModuleBuilder {
96        PayrollModuleBuilder::new()
97    }
98
99    /// Mount ALL generated CRUD endpoints (12 per entity) with NO domain
100    /// validation — the fully **unguarded** surface. A well-formed request can
101    /// create invalid rows or soft-delete a referenced master out from under its
102    /// dependents. Prefer a guarded composition (read + validated writes) for any
103    /// real deployment; use this only in trusted/admin/seeding contexts.
104    pub fn all_crud_routes(&self) -> Router {
105        use presentation::http::{
106            create_compensation_change_read_routes,
107            create_payroll_entry_routes,
108            create_salary_slip_routes,
109            create_salary_slip_line_routes,
110            create_salary_structure_routes,
111            create_salary_component_routes,
112        };
113
114        Router::new()
115            .merge(create_compensation_change_read_routes(self.compensation_change_service.clone()))
116            .merge(create_payroll_entry_routes(self.payroll_entry_service.clone()))
117            .merge(create_salary_slip_routes(self.salary_slip_service.clone()))
118            .merge(create_salary_slip_line_routes(self.salary_slip_line_service.clone()))
119            .merge(create_salary_structure_routes(self.salary_structure_service.clone()))
120            .merge(create_salary_component_routes(self.salary_component_service.clone()))
121    }
122
123    /// Deprecated alias for [`Self::all_crud_routes`]. `routes()` reads like
124    /// "the routes" but mounts UNVALIDATED generic CRUD on every entity — a naive
125    /// mount exposes unguarded writes. Compose a guarded router (read + validated
126    /// writes) for production, or call `all_crud_routes()` to opt into the full
127    /// unguarded surface explicitly.
128    #[deprecated(note = "mounts unvalidated generic CRUD; prefer readonly_routes() + validated writes, or all_crud_routes() for the full/unguarded surface")]
129    pub fn routes(&self) -> Router {
130        self.all_crud_routes()
131    }
132
133    /// Read-only routes for every entity (GET endpoints only) — the safe base.
134    ///
135    /// Generic mutation can't reach here, so this surface cannot bypass a
136    /// validated write service's invariants. Use this as the production base and
137    /// merge validated write routes (or a write service's HTTP layer) onto it.
138    pub fn readonly_routes(&self) -> Router {
139        use presentation::http::{
140            create_compensation_change_read_routes,
141            create_payroll_entry_read_routes,
142            create_salary_slip_read_routes,
143            create_salary_slip_line_read_routes,
144            create_salary_structure_read_routes,
145            create_salary_component_read_routes,
146        };
147
148        Router::new()
149            .merge(create_compensation_change_read_routes(self.compensation_change_service.clone()))
150            .merge(create_payroll_entry_read_routes(self.payroll_entry_service.clone()))
151            .merge(create_salary_slip_read_routes(self.salary_slip_service.clone()))
152            .merge(create_salary_slip_line_read_routes(self.salary_slip_line_service.clone()))
153            .merge(create_salary_structure_read_routes(self.salary_structure_service.clone()))
154            .merge(create_salary_component_read_routes(self.salary_component_service.clone()))
155    }
156
157    // <<< CUSTOM METHODS
158    /// The module-held validated write service (run verbs, computed slips, seams).
159    pub fn payroll_write_service(&self) -> Arc<PayrollWriteService> {
160        self.payroll_write_service.clone()
161    }
162
163    /// Replace the write service at the composition root — the way a host wires real adapters over
164    /// the fail-closed seams (e.g. staging domain events into an outbox) or swaps the pool-default
165    /// input ports for adapters over the attendance/employee exports. Consuming `self` keeps the
166    /// builder chain reading naturally:
167    ///
168    /// ```text
169    /// let payroll = PayrollModule::builder()
170    ///     .with_database(pool.clone())
171    ///     .build()?
172    ///     .with_payroll_write_service(Arc::new(
173    ///         PayrollWriteService::new(pool.clone()).with_event_sink(my_outbox_sink),
174    ///     ));
175    /// ```
176    ///
177    /// Call before mounting routes; the module is consumed once at startup.
178    pub fn with_payroll_write_service(mut self, svc: Arc<PayrollWriteService>) -> Self {
179        self.payroll_write_service = svc;
180        self
181    }
182    // END CUSTOM
183}
184
185/// Builder for PayrollModule
186pub struct PayrollModuleBuilder {
187    db_pool: Option<PgPool>,
188}
189
190impl PayrollModuleBuilder {
191    /// Create a new builder
192    pub fn new() -> Self {
193        Self {
194            db_pool: None,
195        }
196    }
197
198    /// Set the database connection pool
199    pub fn with_database(mut self, pool: PgPool) -> Self {
200        self.db_pool = Some(pool);
201        self
202    }
203
204    // <<< CUSTOM - custom builder methods
205    // END CUSTOM
206
207    /// Build the module with configured dependencies
208    pub fn build(self) -> anyhow::Result<PayrollModule> {
209        let db_pool = self.db_pool
210            .ok_or_else(|| anyhow::anyhow!("Database pool not configured"))?;
211
212        // CompensationChange service
213        let compensation_change_repository = Arc::new(CompensationChangeRepository::new(db_pool.clone()));
214        let compensation_change_service = Arc::new(CompensationChangeService::with_repository(compensation_change_repository.clone()));
215
216        // PayrollEntry service
217        let payroll_entry_repository = Arc::new(PayrollEntryRepository::new(db_pool.clone()));
218        let payroll_entry_service = Arc::new(PayrollEntryService::with_repository(payroll_entry_repository.clone()));
219
220        // SalarySlip service
221        let salary_slip_repository = Arc::new(SalarySlipRepository::new(db_pool.clone()));
222        let salary_slip_service = Arc::new(SalarySlipService::with_repository(salary_slip_repository.clone()));
223
224        // SalarySlipLine service
225        let salary_slip_line_repository = Arc::new(SalarySlipLineRepository::new(db_pool.clone()));
226        let salary_slip_line_service = Arc::new(SalarySlipLineService::with_repository(salary_slip_line_repository.clone()));
227
228        // SalaryStructure service
229        let salary_structure_repository = Arc::new(SalaryStructureRepository::new(db_pool.clone()));
230        let salary_structure_service = Arc::new(SalaryStructureService::with_repository(salary_structure_repository.clone()));
231
232        // SalaryComponent service
233        let salary_component_repository = Arc::new(SalaryComponentRepository::new(db_pool.clone()));
234        let salary_component_service = Arc::new(SalaryComponentService::with_repository(salary_component_repository.clone()));
235
236        // <<< CUSTOM
237        // The validated write path over the same pool: pool-default input ports (works standalone),
238        // seams fail-closed (Unwired GL / Logging events / Unwired remittance) — a composition root
239        // swaps them via `with_payroll_write_service`.
240        let payroll_write_service = Arc::new(PayrollWriteService::new(db_pool.clone()));
241        // END CUSTOM
242
243        Ok(PayrollModule {
244            compensation_change_service,
245            payroll_entry_service,
246            salary_slip_service,
247            salary_slip_line_service,
248            salary_structure_service,
249            salary_component_service,
250            // <<< CUSTOM
251            payroll_write_service,
252            // END CUSTOM
253        })
254    }
255}
256
257impl Default for PayrollModuleBuilder {
258    fn default() -> Self {
259        Self::new()
260    }
261}