webhooks (#1)

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-03-15 23:51:41 +00:00
parent 2ba9bfbf2f
commit db461db270
23 changed files with 981 additions and 65 deletions

View File

@@ -88,6 +88,8 @@ pub struct Channel {
pub logo: Option<String>,
pub logo_position: LogoPosition,
pub logo_opacity: f32,
pub webhook_url: Option<String>,
pub webhook_poll_interval_secs: u32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -113,6 +115,8 @@ impl Channel {
logo: None,
logo_position: LogoPosition::default(),
logo_opacity: 1.0,
webhook_url: None,
webhook_poll_interval_secs: 5,
created_at: now,
updated_at: now,
}

View File

@@ -0,0 +1,112 @@
//! Domain events emitted when important state transitions occur.
//!
//! These are pure data — no I/O, no tokio deps. The transport
//! (tokio::sync::broadcast) lives in `api`; domain only owns the schema.
use uuid::Uuid;
use crate::entities::{Channel, GeneratedSchedule, ScheduledSlot};
/// Events emitted by the application when important state changes occur.
///
/// Must be `Clone + Send + 'static` for use as a `broadcast::channel` item.
#[derive(Clone)]
pub enum DomainEvent {
BroadcastTransition {
channel_id: Uuid,
slot: ScheduledSlot,
},
NoSignal {
channel_id: Uuid,
},
ScheduleGenerated {
channel_id: Uuid,
schedule: GeneratedSchedule,
},
ChannelCreated {
channel: Channel,
},
ChannelUpdated {
channel: Channel,
},
ChannelDeleted {
channel_id: Uuid,
},
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn make_slot() -> crate::entities::ScheduledSlot {
use crate::entities::{MediaItem, ScheduledSlot};
use crate::value_objects::{ContentType, MediaItemId};
use chrono::Utc;
ScheduledSlot {
id: Uuid::new_v4(),
start_at: Utc::now(),
end_at: Utc::now() + chrono::Duration::minutes(30),
item: MediaItem {
id: MediaItemId::new("test-item".to_string()),
title: "Test Movie".to_string(),
content_type: ContentType::Movie,
duration_secs: 1800,
description: None,
genres: vec![],
year: None,
tags: vec![],
series_name: None,
season_number: None,
episode_number: None,
},
source_block_id: Uuid::new_v4(),
}
}
#[test]
fn broadcast_transition_carries_slot() {
let channel_id = Uuid::new_v4();
let slot = make_slot();
let event = DomainEvent::BroadcastTransition { channel_id, slot: slot.clone() };
match event {
DomainEvent::BroadcastTransition { channel_id: cid, slot: s } => {
assert_eq!(cid, channel_id);
assert_eq!(s.item.title, "Test Movie");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn no_signal_carries_channel_id() {
let channel_id = Uuid::new_v4();
let event = DomainEvent::NoSignal { channel_id };
match event {
DomainEvent::NoSignal { channel_id: cid } => assert_eq!(cid, channel_id),
_ => panic!("wrong variant"),
}
}
#[test]
fn schedule_generated_carries_metadata() {
use crate::entities::GeneratedSchedule;
use chrono::Utc;
let channel_id = Uuid::new_v4();
let schedule = GeneratedSchedule {
id: Uuid::new_v4(),
channel_id,
valid_from: Utc::now(),
valid_until: Utc::now() + chrono::Duration::hours(48),
generation: 3,
slots: vec![],
};
let event = DomainEvent::ScheduleGenerated { channel_id, schedule: schedule.clone() };
match event {
DomainEvent::ScheduleGenerated { schedule: s, .. } => {
assert_eq!(s.generation, 3);
assert_eq!(s.slots.len(), 0);
}
_ => panic!("wrong variant"),
}
}
}

View File

@@ -9,11 +9,13 @@ pub mod iptv;
pub mod ports;
pub mod repositories;
pub mod services;
pub mod events;
pub mod value_objects;
// Re-export commonly used types
pub use entities::*;
pub use errors::{DomainError, DomainResult};
pub use events::DomainEvent;
pub use ports::{Collection, IMediaProvider, IProviderRegistry, ProviderCapabilities, SeriesSummary, StreamingProtocol, StreamQuality};
pub use repositories::*;
pub use iptv::{generate_m3u, generate_xmltv};