//! Repository ports (traits) //! //! These traits define the interface for data persistence. //! Implementations live in the infra layer. use std::collections::HashMap; use async_trait::async_trait; use chrono::DateTime; use chrono::Utc; use uuid::Uuid; use crate::entities::{Channel, ChannelConfigSnapshot, GeneratedSchedule, PlaybackRecord, ScheduleConfig, User}; use crate::errors::DomainResult; use crate::value_objects::{BlockId, ChannelId, MediaItemId, UserId}; /// An in-app activity event stored in the database for the admin log view. #[derive(Debug, Clone)] pub struct ActivityEvent { pub id: Uuid, pub timestamp: DateTime, pub event_type: String, pub detail: String, pub channel_id: Option, } /// Repository port for User persistence #[async_trait] pub trait UserRepository: Send + Sync { /// Find a user by their internal ID async fn find_by_id(&self, id: Uuid) -> DomainResult>; /// Find a user by their OIDC subject (used for authentication) async fn find_by_subject(&self, subject: &str) -> DomainResult>; /// Find a user by their email async fn find_by_email(&self, email: &str) -> DomainResult>; /// Save a new user or update an existing one async fn save(&self, user: &User) -> DomainResult<()>; /// Delete a user by their ID async fn delete(&self, id: Uuid) -> DomainResult<()>; /// Count total number of users (used for first-user admin promotion) async fn count_users(&self) -> DomainResult; } #[derive(Debug, Clone)] pub struct ProviderConfigRow { pub provider_type: String, pub config_json: String, pub enabled: bool, pub updated_at: String, } #[async_trait] pub trait ProviderConfigRepository: Send + Sync { async fn get_all(&self) -> DomainResult>; async fn upsert(&self, row: &ProviderConfigRow) -> DomainResult<()>; async fn delete(&self, provider_type: &str) -> DomainResult<()>; } /// Repository port for `Channel` persistence. #[async_trait] pub trait ChannelRepository: Send + Sync { async fn find_by_id(&self, id: ChannelId) -> DomainResult>; async fn find_by_owner(&self, owner_id: UserId) -> DomainResult>; async fn find_all(&self) -> DomainResult>; async fn find_auto_schedule_enabled(&self) -> DomainResult>; /// Insert or update a channel. async fn save(&self, channel: &Channel) -> DomainResult<()>; async fn delete(&self, id: ChannelId) -> DomainResult<()>; /// Snapshot the current config before saving a new one. /// version_num is computed by the infra layer as MAX(version_num)+1 inside a transaction. async fn save_config_snapshot( &self, channel_id: ChannelId, config: &ScheduleConfig, label: Option, ) -> DomainResult; async fn list_config_snapshots( &self, channel_id: ChannelId, ) -> DomainResult>; async fn get_config_snapshot( &self, channel_id: ChannelId, snapshot_id: Uuid, ) -> DomainResult>; async fn patch_config_snapshot_label( &self, channel_id: ChannelId, snapshot_id: Uuid, label: Option, ) -> DomainResult>; } /// Repository port for `GeneratedSchedule` and `PlaybackRecord` persistence. #[async_trait] pub trait ScheduleRepository: Send + Sync { /// Find the schedule whose `[valid_from, valid_until)` window contains `at`. async fn find_active( &self, channel_id: ChannelId, at: DateTime, ) -> DomainResult>; /// Find the most recently generated schedule for a channel. /// Used to derive the next generation number. async fn find_latest( &self, channel_id: ChannelId, ) -> DomainResult>; /// Insert or replace a generated schedule. async fn save(&self, schedule: &GeneratedSchedule) -> DomainResult<()>; /// All playback records for a channel, used by the recycle policy engine. async fn find_playback_history( &self, channel_id: ChannelId, ) -> DomainResult>; async fn save_playback_record(&self, record: &PlaybackRecord) -> DomainResult<()>; /// Return the most recent slot per block_id across ALL schedules for a channel. /// Resilient to any single generation having empty slots for a block. async fn find_last_slot_per_block( &self, channel_id: ChannelId, ) -> DomainResult>; /// List all generated schedule headers for a channel, newest first. async fn list_schedule_history( &self, channel_id: ChannelId, ) -> DomainResult>; /// Fetch a specific schedule with its slots, verifying channel ownership. async fn get_schedule_by_id( &self, channel_id: ChannelId, schedule_id: Uuid, ) -> DomainResult>; /// Delete all schedules with generation > target_generation for this channel. /// Also deletes matching playback_records (no DB cascade between those tables). /// scheduled_slots cascade via FK from generated_schedules. async fn delete_schedules_after( &self, channel_id: ChannelId, target_generation: u32, ) -> DomainResult<()>; } /// Repository port for activity log persistence. #[async_trait] pub trait ActivityLogRepository: Send + Sync { async fn log( &self, event_type: &str, detail: &str, channel_id: Option, ) -> DomainResult<()>; async fn recent(&self, limit: u32) -> DomainResult>; } /// Repository port for transcode settings persistence. #[async_trait] pub trait TranscodeSettingsRepository: Send + Sync { /// Load the persisted cleanup TTL. Returns None if no row exists yet. async fn load_cleanup_ttl(&self) -> DomainResult>; /// Persist the cleanup TTL (upsert — always row id=1). async fn save_cleanup_ttl(&self, hours: u32) -> DomainResult<()>; }