app: person enrichment use case + staleness checks

This commit is contained in:
2026-06-11 13:36:43 +02:00
parent 517a18da8a
commit 371a3cdc46
10 changed files with 70 additions and 8 deletions

View File

@@ -3,11 +3,11 @@ use std::sync::Arc;
use domain::ports::{ use domain::ports::{
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher, GoalRepository, AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher, GoalRepository,
ImportProfileRepository, ImportSessionRepository, MetadataClient, MovieProfileRepository, ImportProfileRepository, ImportSessionRepository, MetadataClient, MovieProfileRepository,
MovieRepository, ObjectStorage, PasswordHasher, PersonCommand, PersonQuery, MovieRepository, ObjectStorage, PasswordHasher, PersonCommand, PersonEnrichmentClient,
PosterFetcherClient, RemoteGoalRepository, RemoteWatchlistRepository, ReviewRepository, PersonQuery, PosterFetcherClient, RemoteGoalRepository, RemoteWatchlistRepository,
SearchCommand, SearchPort, SocialQueryPort, StatsRepository, UserProfileFieldsRepository, ReviewRepository, SearchCommand, SearchPort, SocialQueryPort, StatsRepository,
UserRepository, UserSettingsRepository, WatchEventRepository, WatchlistRepository, UserProfileFieldsRepository, UserRepository, UserSettingsRepository, WatchEventRepository,
WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery, WatchlistRepository, WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery,
}; };
use crate::config::AppConfig; use crate::config::AppConfig;
@@ -51,6 +51,7 @@ pub struct Services {
pub diary_exporter: Arc<dyn DiaryExporter>, pub diary_exporter: Arc<dyn DiaryExporter>,
pub document_parser: Arc<dyn DocumentParser>, pub document_parser: Arc<dyn DocumentParser>,
pub review_logger: Arc<dyn ReviewLogger>, pub review_logger: Arc<dyn ReviewLogger>,
pub person_enrichment: Option<Arc<dyn PersonEnrichmentClient>>,
} }
#[derive(Clone)] #[derive(Clone)]

View File

@@ -0,0 +1,13 @@
use crate::context::AppContext;
use domain::{
errors::DomainError,
models::{PersonEnrichmentData, PersonId},
};
pub async fn execute(
ctx: &AppContext,
person_id: PersonId,
data: PersonEnrichmentData,
) -> Result<(), DomainError> {
ctx.repos.person_command.update_enrichment(&person_id, &data).await
}

View File

@@ -1,11 +1,33 @@
use crate::context::AppContext; use crate::context::AppContext;
use chrono::Utc;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
events::DomainEvent,
models::{Person, PersonId}, models::{Person, PersonId},
}; };
const ENRICHMENT_TTL_DAYS: i64 = 90;
pub async fn execute(ctx: &AppContext, id: PersonId) -> Result<Option<Person>, DomainError> { pub async fn execute(ctx: &AppContext, id: PersonId) -> Result<Option<Person>, DomainError> {
ctx.repos.person_query.get_by_id(&id).await let person = ctx.repos.person_query.get_by_id(&id).await?;
if let Some(ref p) = person {
if should_enrich(p) {
let _ = ctx.services.event_publisher.publish(
&DomainEvent::PersonEnrichmentRequested {
person_id: id,
external_person_id: p.external_id().value().to_string(),
},
).await;
}
}
Ok(person)
}
fn should_enrich(p: &Person) -> bool {
match p.enriched_at() {
None => true,
Some(at) => (Utc::now() - at).num_days() >= ENRICHMENT_TTL_DAYS,
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,11 +1,31 @@
use crate::context::AppContext; use crate::context::AppContext;
use chrono::Utc;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{PersonCredits, PersonId}, events::DomainEvent,
models::{Person, PersonCredits, PersonId},
}; };
const ENRICHMENT_TTL_DAYS: i64 = 90;
pub async fn execute(ctx: &AppContext, id: PersonId) -> Result<PersonCredits, DomainError> { pub async fn execute(ctx: &AppContext, id: PersonId) -> Result<PersonCredits, DomainError> {
ctx.repos.person_query.get_credits(&id).await let credits = ctx.repos.person_query.get_credits(&id).await?;
if should_enrich(&credits.person) {
let _ = ctx.services.event_publisher.publish(
&DomainEvent::PersonEnrichmentRequested {
person_id: id,
external_person_id: credits.person.external_id().value().to_string(),
},
).await;
}
Ok(credits)
}
fn should_enrich(p: &Person) -> bool {
match p.enriched_at() {
None => true,
Some(at) => (Utc::now() - at).num_days() >= ENRICHMENT_TTL_DAYS,
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,2 +1,3 @@
pub mod enrich;
pub mod get; pub mod get;
pub mod get_credits; pub mod get_credits;

View File

@@ -297,6 +297,7 @@ impl TestContextBuilder {
diary_exporter: self.diary_exporter, diary_exporter: self.diary_exporter,
document_parser: self.document_parser, document_parser: self.document_parser,
review_logger: self.review_logger, review_logger: self.review_logger,
person_enrichment: None,
}, },
config: self.config, config: self.config,
} }

View File

@@ -218,6 +218,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
diary_exporter: Arc::new(ExportAdapter) as Arc<dyn DiaryExporter>, diary_exporter: Arc::new(ExportAdapter) as Arc<dyn DiaryExporter>,
document_parser: Arc::new(ImporterDocumentParser) as Arc<dyn DocumentParser>, document_parser: Arc::new(ImporterDocumentParser) as Arc<dyn DocumentParser>,
review_logger, review_logger,
person_enrichment: None,
}, },
config: app_config, config: app_config,
}; };

View File

@@ -781,6 +781,7 @@ pub fn make_test_state(auth_service: Arc<dyn AuthService>) -> crate::state::AppS
diary_exporter: Arc::clone(&repo) as _, diary_exporter: Arc::clone(&repo) as _,
document_parser: Arc::clone(&repo) as _, document_parser: Arc::clone(&repo) as _,
review_logger: Arc::clone(&repo) as _, review_logger: Arc::clone(&repo) as _,
person_enrichment: None,
}, },
config: AppConfig { config: AppConfig {
allow_registration: false, allow_registration: false,

View File

@@ -470,6 +470,7 @@ async fn test_app() -> Router {
diary_exporter: Arc::new(PanicExporter), diary_exporter: Arc::new(PanicExporter),
document_parser: Arc::new(PanicDocumentParser), document_parser: Arc::new(PanicDocumentParser),
review_logger: Arc::new(PanicReviewLogger), review_logger: Arc::new(PanicReviewLogger),
person_enrichment: None,
}, },
config: AppConfig { config: AppConfig {
allow_registration: false, allow_registration: false,

View File

@@ -116,6 +116,7 @@ async fn main() -> anyhow::Result<()> {
diary_exporter: Arc::new(ExportAdapter) as Arc<dyn DiaryExporter>, diary_exporter: Arc::new(ExportAdapter) as Arc<dyn DiaryExporter>,
document_parser: Arc::new(ImporterDocumentParser) as Arc<dyn DocumentParser>, document_parser: Arc::new(ImporterDocumentParser) as Arc<dyn DocumentParser>,
review_logger, review_logger,
person_enrichment: None,
}, },
config: app_config, config: app_config,
}; };