#![allow(dead_code)] //! CMRRA — Canadian Musical Reproduction Rights Agency. //! //! CMRRA (https://www.cmrra.ca) is Canada's primary mechanical rights agency, //! administering reproduction rights for music used in: //! - Physical recordings (CDs, vinyl, cassettes) //! - Digital downloads (iTunes, Beatport, etc.) //! - Streaming (Spotify, Apple Music, Amazon Music, etc.) //! - Ringtones and interactive digital services //! //! CMRRA operates under Section 80 (private copying) and Part VIII of the //! Canadian Copyright Act, and partners with SODRAC for Quebec repertoire. //! Under the CMRRA-SODRAC Processing (CSI) initiative it issues combined //! mechanical + reprographic licences to Canadian DSPs and labels. //! //! This module provides: //! - CMRRA mechanical licence request generation //! - CSI blanket licence rate lookup (CRB Canadian equivalent) //! - Quarterly mechanical royalty statement parsing //! - CMRRA registration number validation //! - DSP reporting file generation (CSV per CMRRA spec) //! //! LangSec: //! - All ISRCs/ISWCs validated by shared parsers before submission. //! - CMRRA registration numbers: 7-digit numeric. //! - Monetary amounts: f64 but capped at CAD 1,000,000 per transaction. //! - All CSV output uses RFC 4180 + CSV-injection prevention. use chrono::{Datelike, Utc}; use serde::{Deserialize, Serialize}; use tracing::{info, instrument, warn}; // ── Config ──────────────────────────────────────────────────────────────────── #[derive(Clone)] pub struct CmrraConfig { pub base_url: String, pub api_key: Option, pub licensee_id: String, pub timeout_secs: u64, pub dev_mode: bool, } impl CmrraConfig { pub fn from_env() -> Self { Self { base_url: std::env::var("CMRRA_BASE_URL") .unwrap_or_else(|_| "https://api.cmrra.ca/v1".into()), api_key: std::env::var("CMRRA_API_KEY").ok(), licensee_id: std::env::var("CMRRA_LICENSEE_ID") .unwrap_or_else(|_| "RETROSYNC-DEV".into()), timeout_secs: std::env::var("CMRRA_TIMEOUT_SECS") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(30), dev_mode: std::env::var("CMRRA_DEV_MODE") .map(|v| v == "1") .unwrap_or(false), } } } // ── CMRRA Registration Number ────────────────────────────────────────────────── /// CMRRA registration number: exactly 7 ASCII digits. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CmrraRegNumber(pub String); impl CmrraRegNumber { pub fn parse(input: &str) -> Option { let s = input.trim().trim_start_matches("CMRRA-"); if s.len() == 7 && s.chars().all(|c| c.is_ascii_digit()) { Some(Self(s.to_string())) } else { None } } } // ── Mechanical Rates (Canada, effective 2024) ───────────────────────────────── /// Canadian statutory mechanical rates (Copyright Board of Canada). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CanadianMechanicalRate { /// Cents per unit for physical recordings (Tariff 22.A) pub physical_per_unit_cad_cents: f64, /// Rate for interactive streaming per stream (Tariff 22.G) pub streaming_per_stream_cad_cents: f64, /// Rate for permanent downloads (Tariff 22.D) pub download_per_track_cad_cents: f64, /// Effective year pub effective_year: i32, /// Copyright Board reference pub board_reference: String, } /// Returns the current Canadian statutory mechanical rates. pub fn current_canadian_rates() -> CanadianMechanicalRate { CanadianMechanicalRate { // Tariff 22.A: CAD 8.3¢/unit for songs ≤5 min (Copyright Board 2022) physical_per_unit_cad_cents: 8.3, // Tariff 22.G: approx CAD 0.012¢/stream (Board ongoing proceedings) streaming_per_stream_cad_cents: 0.012, // Tariff 22.D: CAD 10.2¢/download download_per_track_cad_cents: 10.2, effective_year: 2024, board_reference: "Copyright Board of Canada Tariff 22 (2022–2024)".into(), } } // ── Licence Request ──────────────────────────────────────────────────────────── /// Supported use types for CMRRA mechanical licences. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CmrraUseType { PhysicalRecording, PermanentDownload, InteractiveStreaming, LimitedDownload, Ringtone, PrivateCopying, } impl CmrraUseType { pub fn tariff_ref(&self) -> &'static str { match self { Self::PhysicalRecording => "Tariff 22.A", Self::PermanentDownload => "Tariff 22.D", Self::InteractiveStreaming => "Tariff 22.G", Self::LimitedDownload => "Tariff 22.F", Self::Ringtone => "Tariff 24", Self::PrivateCopying => "Tariff 8", } } } /// A mechanical licence request to CMRRA. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CmrraLicenceRequest { pub isrc: String, pub iswc: Option, pub title: String, pub artist: String, pub composer: String, pub publisher: String, pub cmrra_reg: Option, pub use_type: CmrraUseType, pub territory: String, pub expected_units: u64, pub release_date: String, } /// CMRRA licence response. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CmrraLicenceResponse { pub licence_number: String, pub isrc: String, pub use_type: CmrraUseType, pub rate_cad_cents: f64, pub total_due_cad: f64, pub quarter: String, pub status: CmrraLicenceStatus, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CmrraLicenceStatus { Approved, Pending, Rejected, ManualReview, } /// Request a mechanical licence from CMRRA (or simulate in dev mode). #[instrument(skip(config))] pub async fn request_licence( config: &CmrraConfig, req: &CmrraLicenceRequest, ) -> anyhow::Result { info!(isrc=%req.isrc, use_type=?req.use_type, "CMRRA licence request"); if config.dev_mode { let rate = current_canadian_rates(); let rate_cad = match req.use_type { CmrraUseType::PhysicalRecording => rate.physical_per_unit_cad_cents, CmrraUseType::PermanentDownload => rate.download_per_track_cad_cents, CmrraUseType::InteractiveStreaming => rate.streaming_per_stream_cad_cents, _ => rate.physical_per_unit_cad_cents, }; let total = (req.expected_units as f64 * rate_cad) / 100.0; let now = Utc::now(); return Ok(CmrraLicenceResponse { licence_number: format!("CMRRA-DEV-{:08X}", now.timestamp() as u32), isrc: req.isrc.clone(), use_type: req.use_type.clone(), rate_cad_cents: rate_cad, total_due_cad: total, quarter: format!("{}Q{}", now.year(), now.month().div_ceil(3)), status: CmrraLicenceStatus::Approved, }); } if config.api_key.is_none() { anyhow::bail!("CMRRA_API_KEY not set; cannot request live licence"); } let url = format!("{}/licences", config.base_url); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(config.timeout_secs)) .user_agent("Retrosync/1.0 CMRRA-Client") .build()?; let resp = client .post(&url) .header( "Authorization", format!("Bearer {}", config.api_key.as_deref().unwrap_or("")), ) .header("X-Licensee-Id", &config.licensee_id) .json(req) .send() .await?; if !resp.status().is_success() { let status = resp.status().as_u16(); warn!(isrc=%req.isrc, status, "CMRRA licence request failed"); anyhow::bail!("CMRRA API error: HTTP {status}"); } let response: CmrraLicenceResponse = resp.json().await?; Ok(response) } // ── Quarterly Royalty Statement ──────────────────────────────────────────────── /// A single line in a CMRRA quarterly royalty statement. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CmrraStatementLine { pub isrc: String, pub title: String, pub units: u64, pub rate_cad_cents: f64, pub royalty_cad: f64, pub use_type: String, pub period: String, } /// Generate CMRRA quarterly royalty statement CSV per CMRRA DSP reporting spec. /// /// CSV format: ISRC, Title, Units, Rate (CAD cents), Royalty (CAD), Use Type, Period pub fn generate_quarterly_csv(lines: &[CmrraStatementLine]) -> String { let mut out = String::new(); out.push_str("ISRC,Title,Units,Rate_CAD_Cents,Royalty_CAD,UseType,Period\r\n"); for line in lines { out.push_str(&csv_field(&line.isrc)); out.push(','); out.push_str(&csv_field(&line.title)); out.push(','); out.push_str(&line.units.to_string()); out.push(','); out.push_str(&format!("{:.4}", line.rate_cad_cents)); out.push(','); out.push_str(&format!("{:.2}", line.royalty_cad)); out.push(','); out.push_str(&csv_field(&line.use_type)); out.push(','); out.push_str(&csv_field(&line.period)); out.push_str("\r\n"); } out } /// RFC 4180 CSV field escaping with CSV-injection prevention. fn csv_field(s: &str) -> String { // Prevent CSV injection: fields starting with =,+,-,@ are prefixed with tab let safe = if s.starts_with(['=', '+', '-', '@']) { format!("\t{s}") } else { s.to_string() }; if safe.contains([',', '"', '\r', '\n']) { format!("\"{}\"", safe.replace('"', "\"\"")) } else { safe } } // ── CMRRA-SODRAC (CSI) blanket licence status ───────────────────────────────── /// CSI (CMRRA-SODRAC Inc.) blanket licence for Canadian DSPs. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CsiBlanketLicence { pub licensee: String, pub licence_type: String, pub territories: Vec, pub repertoire_coverage: String, pub effective_date: String, pub expiry_date: Option, pub annual_minimum_cad: f64, } /// Returns metadata about CSI blanket licence applicability. pub fn csi_blanket_info() -> CsiBlanketLicence { CsiBlanketLicence { licensee: "Retrosync Media Group".into(), licence_type: "CSI Online Music Services Licence (OMSL)".into(), territories: vec!["CA".into()], repertoire_coverage: "CMRRA + SODRAC combined mechanical repertoire".into(), effective_date: "2024-01-01".into(), expiry_date: None, annual_minimum_cad: 500.0, } }