backbone-mailing 0.3.30

Email marketing — mass-mail engine: audiences, subscriptions, hand-set mailing lifecycle, seeded A/B testing, per-recipient traces, cycle-44 bridge targets (Odoo mass_mailing port)
//! Mailing 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 - State Machine
pub use domain::state_machine::*;

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

// Re-exports - Application services
pub use application::service::MailingAbTestService;
pub use application::service::MailingAudienceService;
pub use application::service::MailingContactService;
pub use application::service::MailingSubscriptionService;
pub use application::service::OptOutReasonService;
pub use application::service::MailingService;
pub use application::service::MailingTraceService;
pub use application::service::MailingFilterService;

// Re-exports - Validation
pub use application::validator::{ValidationError, ValidationResult};

use std::sync::Arc;
use axum::Router;
use sqlx::PgPool;

/// Mailing module configuration
///
/// Use the builder pattern to configure and register this module:
///
/// ```text
/// let mailing = MailingModule::builder()
///     .with_database(pool.clone())
///     .build()?;
///
/// // Unguarded full CRUD (trusted/admin); compose a guarded router for production.
/// let router = mailing.all_crud_routes();
/// ```
pub struct MailingModule {
    pub(crate) mailing_ab_test_service: Arc<MailingAbTestService>,
    pub(crate) mailing_audience_service: Arc<MailingAudienceService>,
    pub(crate) mailing_contact_service: Arc<MailingContactService>,
    pub(crate) mailing_subscription_service: Arc<MailingSubscriptionService>,
    pub(crate) opt_out_reason_service: Arc<OptOutReasonService>,
    pub(crate) mailing_service: Arc<MailingService>,
    pub(crate) mailing_trace_service: Arc<MailingTraceService>,
    pub(crate) mailing_filter_service: Arc<MailingFilterService>,
    // <<< CUSTOM FIELDS
    pub(crate) trace_clicks: crate::application::service::trace_click_ports::TraceClickSlot,
    pub(crate) trace_route_service:
        std::sync::Arc<crate::application::service::trace_route_service::TraceRouteService>,
    // END CUSTOM
}

impl MailingModule {
    /// Create a new module builder
    pub fn builder() -> MailingModuleBuilder {
        MailingModuleBuilder::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_mailing_ab_test_routes,
            create_mailing_audience_routes,
            create_mailing_contact_routes,
            create_mailing_subscription_routes,
            create_opt_out_reason_routes,
            create_mailing_read_routes,
            create_mailing_trace_read_routes,
            create_mailing_filter_routes,
        };

        Router::new()
            .merge(create_mailing_ab_test_routes(self.mailing_ab_test_service.clone()))
            .merge(create_mailing_audience_routes(self.mailing_audience_service.clone()))
            .merge(create_mailing_contact_routes(self.mailing_contact_service.clone()))
            .merge(create_mailing_subscription_routes(self.mailing_subscription_service.clone()))
            .merge(create_opt_out_reason_routes(self.opt_out_reason_service.clone()))
            // Mailing: hand_set lifecycle — the state field moves only through the
            // module's validated verbs; generic writes cannot reach it, so only the
            // read surface mounts here.
            .merge(create_mailing_read_routes(self.mailing_service.clone()))
            // MailingTrace: hand_set lifecycle — the state field moves only through the
            // module's validated verbs; generic writes cannot reach it, so only the
            // read surface mounts here.
            .merge(create_mailing_trace_read_routes(self.mailing_trace_service.clone()))
            .merge(create_mailing_filter_routes(self.mailing_filter_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_mailing_ab_test_read_routes,
            create_mailing_audience_read_routes,
            create_mailing_contact_read_routes,
            create_mailing_subscription_read_routes,
            create_opt_out_reason_read_routes,
            create_mailing_read_routes,
            create_mailing_trace_read_routes,
            create_mailing_filter_read_routes,
        };

        Router::new()
            .merge(create_mailing_ab_test_read_routes(self.mailing_ab_test_service.clone()))
            .merge(create_mailing_audience_read_routes(self.mailing_audience_service.clone()))
            .merge(create_mailing_contact_read_routes(self.mailing_contact_service.clone()))
            .merge(create_mailing_subscription_read_routes(self.mailing_subscription_service.clone()))
            .merge(create_opt_out_reason_read_routes(self.opt_out_reason_service.clone()))
            .merge(create_mailing_read_routes(self.mailing_service.clone()))
            .merge(create_mailing_trace_read_routes(self.mailing_trace_service.clone()))
            .merge(create_mailing_filter_read_routes(self.mailing_filter_service.clone()))
    }

    // <<< CUSTOM METHODS
    /// Compose the short-link click seam for the public trace-route family
    /// (deny-by-default until called — the PhoneBookPort shape): the ONE
    /// registered implementation answers code attribution + click minting
    /// by wrapping the short-link module's public seams.
    pub fn set_trace_click_port(
        &self,
        port: std::sync::Arc<dyn crate::application::service::trace_click_ports::TraceClickPort>,
    ) {
        self.trace_clicks.install(port);
    }

    /// The public `/r/:code/m/:trace` trace-route family (click + open
    /// pixel + unsubscribe): a BARE capability mount throttled 120/min per
    /// code — merge at the site ROOT, next to the short-link module's own
    /// `/r/:code` group (distinct path shapes; no shadowing).
    pub fn public_trace_routes(&self) -> Router {
        crate::presentation::http::trace_routes::public_composer(
            self.trace_route_service.clone(),
        )
    }
    // END CUSTOM
}

/// Builder for MailingModule
pub struct MailingModuleBuilder {
    db_pool: Option<PgPool>,
}

impl MailingModuleBuilder {
    /// 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<MailingModule> {
        let db_pool = self.db_pool
            .ok_or_else(|| anyhow::anyhow!("Database pool not configured"))?;

        // MailingAbTest service
        let mailing_ab_test_repository = Arc::new(MailingAbTestRepository::new(db_pool.clone()));
        let mailing_ab_test_service = Arc::new(MailingAbTestService::with_repository(mailing_ab_test_repository.clone()));

        // MailingAudience service
        let mailing_audience_repository = Arc::new(MailingAudienceRepository::new(db_pool.clone()));
        let mailing_audience_service = Arc::new(MailingAudienceService::with_repository(mailing_audience_repository.clone()));

        // MailingContact service
        let mailing_contact_repository = Arc::new(MailingContactRepository::new(db_pool.clone()));
        let mailing_contact_service = Arc::new(MailingContactService::with_repository(mailing_contact_repository.clone()));

        // MailingSubscription service
        let mailing_subscription_repository = Arc::new(MailingSubscriptionRepository::new(db_pool.clone()));
        let mailing_subscription_service = Arc::new(MailingSubscriptionService::with_repository(mailing_subscription_repository.clone()));

        // OptOutReason service
        let opt_out_reason_repository = Arc::new(OptOutReasonRepository::new(db_pool.clone()));
        let opt_out_reason_service = Arc::new(OptOutReasonService::with_repository(opt_out_reason_repository.clone()));

        // Mailing service
        let mailing_repository = Arc::new(MailingRepository::new(db_pool.clone()));
        let mailing_service = Arc::new(MailingService::with_repository(mailing_repository.clone()));

        // MailingTrace service
        let mailing_trace_repository = Arc::new(MailingTraceRepository::new(db_pool.clone()));
        let mailing_trace_service = Arc::new(MailingTraceService::with_repository(mailing_trace_repository.clone()));

        // MailingFilter service
        let mailing_filter_repository = Arc::new(MailingFilterRepository::new(db_pool.clone()));
        let mailing_filter_service = Arc::new(MailingFilterService::with_repository(mailing_filter_repository.clone()));

        // <<< CUSTOM
        let trace_clicks =
            crate::application::service::trace_click_ports::TraceClickSlot::default();
        let trace_route_service = std::sync::Arc::new(
            crate::application::service::trace_route_service::TraceRouteService::new(
                db_pool.clone(),
                trace_clicks.clone(),
            ),
        );
        // END CUSTOM

        Ok(MailingModule {
            mailing_ab_test_service,
            mailing_audience_service,
            mailing_contact_service,
            mailing_subscription_service,
            opt_out_reason_service,
            mailing_service,
            mailing_trace_service,
            mailing_filter_service,
            // <<< CUSTOM
            trace_clicks,
            trace_route_service,
            // END CUSTOM
        })
    }
}

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