#![recursion_limit = "1024"]
#![allow(unused_imports)]
pub mod application;
pub mod domain;
pub mod exports;
pub mod handlers;
pub mod infrastructure;
pub mod presentation;
pub mod seeders;
pub use domain::entity::*;
pub use infrastructure::persistence::*;
pub use application::service::DigestDigestService;
pub use application::service::DigestSubscriptionService;
pub use application::service::DigestDigestKpiService;
pub use application::service::DigestTipService;
pub use application::service::DigestTipUserService;
use std::sync::Arc;
use axum::Router;
use sqlx::PgPool;
use uuid::Uuid;
pub struct DigestModule {
pub(crate) digest_digest_service: Arc<DigestDigestService>,
pub(crate) digest_subscription_service: Arc<DigestSubscriptionService>,
pub(crate) digest_digest_kpi_service: Arc<DigestDigestKpiService>,
pub(crate) digest_tip_service: Arc<DigestTipService>,
pub(crate) digest_tip_user_service: Arc<DigestTipUserService>,
pub(crate) registry: Arc<application::service::kpi_registry::KpiRegistry>,
pub(crate) recipient_port: application::service::engagement_port::RecipientContextSlot,
pub(crate) write_service: Arc<application::service::DigestWriteService>,
pub(crate) render_service: Arc<application::service::DigestRenderService>,
pub(crate) cron_service: Arc<application::service::DigestCronService>,
pub(crate) unsubscribe: Option<Arc<application::service::UnsubscribeService>>,
pub(crate) default_digest: Option<Uuid>,
}
impl DigestModule {
pub fn builder() -> DigestModuleBuilder {
DigestModuleBuilder::new()
}
pub fn all_crud_routes(&self) -> Router {
use presentation::http::{
create_digest_digest_routes,
create_digest_subscription_routes,
create_digest_digest_kpi_routes,
create_digest_tip_routes,
create_digest_tip_user_routes,
};
Router::new()
.merge(create_digest_digest_routes(self.digest_digest_service.clone()))
.merge(create_digest_subscription_routes(self.digest_subscription_service.clone()))
.merge(create_digest_digest_kpi_routes(self.digest_digest_kpi_service.clone()))
.merge(create_digest_tip_routes(self.digest_tip_service.clone()))
.merge(create_digest_tip_user_routes(self.digest_tip_user_service.clone()))
}
#[deprecated(note = "mounts unvalidated generic CRUD; prefer readonly_routes() + the write-service verbs, or all_crud_routes() for the full/unguarded surface")]
pub fn routes(&self) -> Router {
self.all_crud_routes()
}
pub fn readonly_routes(&self) -> Router {
use presentation::http::{
create_digest_digest_read_routes,
create_digest_subscription_read_routes,
create_digest_digest_kpi_read_routes,
create_digest_tip_read_routes,
create_digest_tip_user_read_routes,
};
Router::new()
.merge(create_digest_digest_read_routes(self.digest_digest_service.clone()))
.merge(create_digest_subscription_read_routes(self.digest_subscription_service.clone()))
.merge(create_digest_digest_kpi_read_routes(self.digest_digest_kpi_service.clone()))
.merge(create_digest_tip_read_routes(self.digest_tip_service.clone()))
.merge(create_digest_tip_user_read_routes(self.digest_tip_user_service.clone()))
}
pub fn kpi_registry(&self) -> &Arc<application::service::kpi_registry::KpiRegistry> {
&self.registry
}
pub fn register_kpi(
&self,
def: application::service::kpi_registry::KpiDefinition,
) -> Result<(), application::service::kpi_registry::KpiRegistrationError> {
self.registry.register(def)
}
pub fn set_recipient_port(
&self,
port: Arc<dyn application::service::engagement_port::RecipientContextPort>,
) {
self.recipient_port.install(port);
}
pub fn write_service(&self) -> &Arc<application::service::DigestWriteService> {
&self.write_service
}
pub fn render_service(&self) -> &Arc<application::service::DigestRenderService> {
&self.render_service
}
pub fn cron_service(&self) -> &Arc<application::service::DigestCronService> {
&self.cron_service
}
pub fn unsubscribe_service(&self) -> Option<&Arc<application::service::UnsubscribeService>> {
self.unsubscribe.as_ref()
}
pub fn user_created_handler(&self) -> Arc<application::service::UserCreatedHandler> {
Arc::new(application::service::UserCreatedHandler::new(
self.write_service.clone(),
self.recipient_port.clone(),
self.default_digest,
))
}
pub fn digest_public_routes(self: &Arc<Self>) -> Router {
crate::presentation::http::public_routes::public_composer()
.with_state(Arc::clone(self))
}
}
pub struct DigestModuleBuilder {
db_pool: Option<PgPool>,
token_secret: Option<Vec<u8>>,
public_base_url: Option<String>,
default_digest: Option<Uuid>,
mail_seam: Option<Arc<dyn application::service::DigestMailSeam>>,
recipient_port: Option<Arc<dyn application::service::engagement_port::RecipientContextPort>>,
kpis: Vec<application::service::kpi_registry::KpiDefinition>,
}
impl DigestModuleBuilder {
pub fn new() -> Self {
Self {
db_pool: None,
token_secret: None,
public_base_url: None,
default_digest: None,
mail_seam: None,
recipient_port: None,
kpis: Vec::new(),
}
}
pub fn with_database(mut self, pool: PgPool) -> Self {
self.db_pool = Some(pool);
self
}
pub fn with_token_secret(mut self, secret: impl Into<Vec<u8>>) -> Self {
self.token_secret = Some(secret.into());
self
}
pub fn with_public_base_url(mut self, base: impl Into<String>) -> Self {
self.public_base_url = Some(base.into());
self
}
pub fn with_default_digest(mut self, digest_id: Uuid) -> Self {
self.default_digest = Some(digest_id);
self
}
pub fn with_mail_seam(
mut self,
seam: Arc<dyn application::service::DigestMailSeam>,
) -> Self {
self.mail_seam = Some(seam);
self
}
pub fn with_recipient_port(
mut self,
port: Arc<dyn application::service::engagement_port::RecipientContextPort>,
) -> Self {
self.recipient_port = Some(port);
self
}
pub fn register_kpi(
mut self,
def: application::service::kpi_registry::KpiDefinition,
) -> Self {
self.kpis.push(def);
self
}
pub fn build(self) -> anyhow::Result<DigestModule> {
let db_pool = self
.db_pool
.ok_or_else(|| anyhow::anyhow!("Database pool not configured"))?;
let digest_digest_repository = Arc::new(DigestDigestRepository::new(db_pool.clone()));
let digest_digest_service = Arc::new(DigestDigestService::with_repository(digest_digest_repository.clone()));
let digest_subscription_repository = Arc::new(DigestSubscriptionRepository::new(db_pool.clone()));
let digest_subscription_service = Arc::new(DigestSubscriptionService::with_repository(digest_subscription_repository.clone()));
let digest_digest_kpi_repository = Arc::new(DigestDigestKpiRepository::new(db_pool.clone()));
let digest_digest_kpi_service = Arc::new(DigestDigestKpiService::with_repository(digest_digest_kpi_repository.clone()));
let digest_tip_repository = Arc::new(DigestTipRepository::new(db_pool.clone()));
let digest_tip_service = Arc::new(DigestTipService::with_repository(digest_tip_repository.clone()));
let digest_tip_user_repository = Arc::new(DigestTipUserRepository::new(db_pool.clone()));
let digest_tip_user_service = Arc::new(DigestTipUserService::with_repository(digest_tip_user_repository.clone()));
let registry = Arc::new(application::service::kpi_registry::KpiRegistry::new());
let recipient_port = application::service::engagement_port::RecipientContextSlot::default();
if let Some(port) = self.recipient_port {
recipient_port.install(port);
}
application::service::base_kpis::register_base_kpis(®istry, recipient_port.clone());
for def in self.kpis {
registry
.register(def)
.map_err(|e| anyhow::anyhow!("KPI registration refused: {e}"))?;
}
let write_service = Arc::new(application::service::DigestWriteService::new(
db_pool.clone(),
registry.clone(),
));
let mail_seam: Arc<dyn application::service::DigestMailSeam> =
self.mail_seam.unwrap_or_else(|| {
Arc::new(application::service::digest_mail_seam::DefaultMailSeam::new(
db_pool.clone(),
))
});
let secret = self.token_secret.clone().or_else(|| {
std::env::var(application::service::unsubscribe_service::DIGEST_TOKEN_SECRET_ENV)
.ok()
.map(|s| s.into_bytes())
});
let unsubscribe = secret.map(|s| {
Arc::new(application::service::UnsubscribeService::with_secret(
db_pool.clone(),
&s,
))
});
if unsubscribe.is_none() {
tracing::warn!(
"no unsubscribe token secret configured (builder or {}); digest renders will refuse until one is set",
application::service::unsubscribe_service::DIGEST_TOKEN_SECRET_ENV
);
}
let render_service = Arc::new(application::service::DigestRenderService::new(
db_pool.clone(),
registry.clone(),
recipient_port.clone(),
mail_seam,
unsubscribe.clone(),
self.public_base_url
.clone()
.unwrap_or_else(|| "http://localhost:8080".to_string()),
));
let cron_service = Arc::new(application::service::DigestCronService::new(
db_pool.clone(),
render_service.clone(),
recipient_port.clone(),
));
Ok(DigestModule {
digest_digest_service,
digest_subscription_service,
digest_digest_kpi_service,
digest_tip_service,
digest_tip_user_service,
registry,
recipient_port,
write_service,
render_service,
cron_service,
unsubscribe,
default_digest: self.default_digest,
})
}
}
impl Default for DigestModuleBuilder {
fn default() -> Self {
Self::new()
}
}