//! Global Trade Management System (GTMS) integration. //! //! Scope for a digital music platform: //! • Work classification: ECCN (Export Control Classification Number) and //! HS code assignment for physical merch, recording media, and digital goods. //! • Distribution screening: cross-border digital delivery routed through //! GTMS sanctions/embargo checks before DSP delivery or society submission. //! • Export declaration: EEI (Electronic Export Information) stubs for //! physical shipments (vinyl pressings, merch) via AES / CBP ACE. //! • Denied Party Screening (DPS): checks payees against: //! – OFAC SDN / Consolidated Sanctions List //! – EU Consolidated List (EUR-Lex) //! – UN Security Council sanctions //! – UK HM Treasury financial sanctions //! – BIS Entity List / Unverified List //! • Incoterms 2020 annotation on physical shipments. //! //! Integration targets: //! • SAP GTS (Global Trade Services) via RFC/BAPI or REST API //! (SAP_GTS_SANCTIONS / SAP_GTS_CLASSIFICATION OData services). //! • Thomson Reuters World-Check / Refinitiv (REST) — DPS fallback. //! • US Census Bureau AES Direct (EEI filing). //! • EU ICS2 (Import Control System 2) for EU entry declarations. //! //! Zero Trust: all GTMS API calls use mTLS client cert. //! LangSec: all HS codes validated against 6-digit WCO pattern. //! ISO 9001 §7.5: all screening results and classifications logged. use crate::AppState; use axum::{ extract::{Path, State}, http::StatusCode, response::Json, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Mutex; use tracing::{info, warn}; // ── HS / ECCN code validation (LangSec) ───────────────────────────────────── /// Validate a WCO Harmonized System code (6-digit minimum: NNNN.NN). #[allow(dead_code)] pub fn validate_hs_code(hs: &str) -> bool { let digits: String = hs.chars().filter(|c| c.is_ascii_digit()).collect(); digits.len() >= 6 } /// Validate an ECCN (Export Control Classification Number). /// Format: \d[A-Z]\d\d\d[a-z]? e.g. "5E002", "EAR99", "AT010" #[allow(dead_code)] pub fn validate_eccn(eccn: &str) -> bool { if eccn == "EAR99" || eccn == "NLR" { return true; } let b = eccn.as_bytes(); b.len() >= 5 && b[0].is_ascii_digit() && b[1].is_ascii_uppercase() && b[2].is_ascii_digit() && b[3].is_ascii_digit() && b[4].is_ascii_digit() } // ── Incoterms 2020 ──────────────────────────────────────────────────────────── #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum Incoterm { Exw, // Ex Works Fca, // Free Carrier Cpt, // Carriage Paid To Cip, // Carriage and Insurance Paid To Dap, // Delivered at Place Dpu, // Delivered at Place Unloaded Ddp, // Delivered Duty Paid Fas, // Free Alongside Ship Fob, // Free On Board Cfr, // Cost and Freight Cif, // Cost, Insurance and Freight } impl Incoterm { pub fn code(&self) -> &'static str { match self { Self::Exw => "EXW", Self::Fca => "FCA", Self::Cpt => "CPT", Self::Cip => "CIP", Self::Dap => "DAP", Self::Dpu => "DPU", Self::Ddp => "DDP", Self::Fas => "FAS", Self::Fob => "FOB", Self::Cfr => "CFR", Self::Cif => "CIF", } } pub fn transport_mode(&self) -> &'static str { match self { Self::Fas | Self::Fob | Self::Cfr | Self::Cif => "SEA", _ => "ANY", } } } // ── Sanctioned jurisdictions (OFAC + EU + UN programs) ─────────────────────── // Kept as a compiled-in list; production integrations call a live DPS API. const EMBARGOED_COUNTRIES: &[&str] = &[ "CU", // Cuba — OFAC comprehensive embargo "IR", // Iran — OFAC ITSR "KP", // North Korea — UN 1718 / OFAC NKSR "RU", // Russia — OFAC SDN + EU/UK financial sanctions "BY", // Belarus — EU restrictive measures "SY", // Syria — OFAC SYSR "VE", // Venezuela — OFAC EO 13850 "MM", // Myanmar — OFAC / UK "ZW", // Zimbabwe — OFAC ZDERA "SS", // South Sudan — UN arms embargo "CF", // Central African Republic — UN arms embargo "LY", // Libya — UN arms embargo "SD", // Sudan — OFAC "SO", // Somalia — UN arms embargo "YE", // Yemen — UN arms embargo "HT", // Haiti — UN targeted sanctions "ML", // Mali — UN targeted sanctions "NI", // Nicaragua — OFAC EO 13851 ]; /// Restricted digital distribution territories (not full embargoes but /// require heightened compliance review — OFAC 50% rule, deferred access). const RESTRICTED_TERRITORIES: &[&str] = &[ "CN", // China — BIS Entity List exposure, music licensing restrictions "IN", // India — FEMA remittance limits on royalty payments "NG", // Nigeria — CBN FX restrictions on royalty repatriation "EG", // Egypt — royalty remittance requires CBE approval "PK", // Pakistan — SBP restrictions "BD", // Bangladesh — BB foreign remittance controls "VN", // Vietnam — State Bank approval for licensing income ]; // ── Domain types ────────────────────────────────────────────────────────────── /// Classification request — a musical work or physical product to classify. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClassificationRequest { pub isrc: Option, pub iswc: Option, pub title: String, pub product_type: ProductType, pub countries: Vec, // destination ISO 3166-1 alpha-2 codes pub sender_id: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ProductType { DigitalDownload, // EAR99 / 5E002 depending on DRM StreamingLicense, // EAR99 — no physical export VinylRecord, // HS 8524.91 (analog audio media) Cd, // HS 8523.49 Usb, // HS 8523.51 Merchandise, // HS varies; requires specific classification PublishingLicense, // EAR99 — intangible MasterRecording, // EAR99 unless DRM technology } impl ProductType { /// Preliminary ECCN based on product type. /// Final ECCN requires full technical review; this is a default assignment. pub fn preliminary_eccn(&self) -> &'static str { match self { Self::DigitalDownload => "EAR99", // unless encryption >64-bit keys Self::StreamingLicense => "EAR99", Self::VinylRecord => "EAR99", Self::Cd => "EAR99", Self::Usb => "EAR99", // re-review if >1TB encrypted Self::Merchandise => "EAR99", Self::PublishingLicense => "EAR99", Self::MasterRecording => "EAR99", } } /// HS code (6-digit WCO) for physical goods; None for digital/licensing. pub fn hs_code(&self) -> Option<&'static str> { match self { Self::VinylRecord => Some("852491"), // gramophone records Self::Cd => Some("852349"), // optical media Self::Usb => Some("852351"), // flash memory media Self::Merchandise => None, // requires specific classification _ => None, // digital / intangible } } } /// Classification result. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClassificationResult { pub request_id: String, pub title: String, pub product_type: ProductType, pub eccn: String, pub hs_code: Option, pub ear_jurisdiction: bool, // true = subject to EAR (US) pub itar_jurisdiction: bool, // true = subject to ITAR (always false for music) pub license_required: bool, // true = export licence needed for some destinations pub licence_exception: Option, // e.g. "TSR", "STA", "TMP" pub restricted_countries: Vec, // subset of requested countries requiring review pub embargoed_countries: Vec, // subset under comprehensive embargo pub incoterm: Option, pub notes: String, pub classified_at: String, } /// Distribution screening request — check if a set of payees/territories /// can receive a royalty payment or content delivery. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScreeningRequest { pub screening_id: String, pub payee_name: String, pub payee_country: String, pub payee_vendor_id: Option, pub territories: Vec, // delivery territories pub amount_usd: f64, pub isrc: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ScreeningOutcome { Clear, // no matches — proceed ReviewRequired, // partial match or restricted territory — human review Blocked, // embargoed / SDN match — do not proceed } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScreeningResult { pub screening_id: String, pub outcome: ScreeningOutcome, pub blocked_reasons: Vec, pub review_reasons: Vec, pub embargoed_territories: Vec, pub restricted_territories: Vec, pub dps_checked: bool, // true = live DPS API was called pub screened_at: String, } /// Export declaration for physical shipments. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExportDeclaration { pub declaration_id: String, pub shipper: String, pub consignee: String, pub destination: String, // ISO 3166-1 alpha-2 pub hs_code: String, pub eccn: String, pub incoterm: Incoterm, pub gross_value_usd: f64, pub quantity: u32, pub unit: String, // e.g. "PCS", "KG" pub eei_status: EeiStatus, pub aes_itn: Option, // AES Internal Transaction Number pub created_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum EeiStatus { NotRequired, // value < $2,500 or EEI exemption applies Pending, // awaiting AES filing Filed, // AES ITN assigned Rejected, // AES rejected — correction required } // ── Store ───────────────────────────────────────────────────────────────────── pub struct GtmsStore { classifications: Mutex>, screenings: Mutex>, declarations: Mutex>, } impl Default for GtmsStore { fn default() -> Self { Self::new() } } impl GtmsStore { pub fn new() -> Self { Self { classifications: Mutex::new(HashMap::new()), screenings: Mutex::new(HashMap::new()), declarations: Mutex::new(HashMap::new()), } } pub fn save_classification(&self, r: ClassificationResult) { if let Ok(mut m) = self.classifications.lock() { m.insert(r.request_id.clone(), r); } } pub fn save_screening(&self, r: ScreeningResult) { if let Ok(mut m) = self.screenings.lock() { m.insert(r.screening_id.clone(), r); } } pub fn get_declaration(&self, id: &str) -> Option { self.declarations.lock().ok()?.get(id).cloned() } pub fn save_declaration(&self, d: ExportDeclaration) { if let Ok(mut m) = self.declarations.lock() { m.insert(d.declaration_id.clone(), d); } } } // ── Core logic ──────────────────────────────────────────────────────────────── fn new_id() -> String { // Deterministic ID from timestamp + counter (no uuid dep) let ts = chrono::Utc::now().format("%Y%m%d%H%M%S%6f").to_string(); format!("GTMS-{ts}") } fn now_iso() -> String { chrono::Utc::now().to_rfc3339() } /// Classify a work/product and determine ECCN, HS code, and export control posture. fn classify(req: &ClassificationRequest) -> ClassificationResult { let eccn = req.product_type.preliminary_eccn().to_string(); let hs_code = req.product_type.hs_code().map(str::to_string); let ear = true; // all US-origin or US-person transactions subject to EAR let itar = false; // music is never ITAR (USML categories I-XXI don't cover it) let embargoed: Vec = req .countries .iter() .filter(|c| EMBARGOED_COUNTRIES.contains(&c.as_str())) .cloned() .collect(); let restricted: Vec = req .countries .iter() .filter(|c| RESTRICTED_TERRITORIES.contains(&c.as_str())) .cloned() .collect(); // EAR99 items: no licence required except to embargoed/sanctioned destinations let license_required = !embargoed.is_empty(); let licence_exception = if !license_required && eccn == "EAR99" { Some("NLR".into()) // No Licence Required } else { None }; let incoterm = match req.product_type { ProductType::VinylRecord | ProductType::Cd | ProductType::Usb => Some(Incoterm::Dap), // default for physical goods _ => None, }; let notes = if embargoed.is_empty() && restricted.is_empty() { format!( "EAR99 — no licence required for {} destination(s)", req.countries.len() ) } else if license_required { format!( "LICENCE REQUIRED for embargoed destination(s): {}. Do not ship/deliver.", embargoed.join(", ") ) } else { format!( "Restricted territory review required: {}", restricted.join(", ") ) }; ClassificationResult { request_id: new_id(), title: req.title.clone(), product_type: req.product_type.clone(), eccn, hs_code, ear_jurisdiction: ear, itar_jurisdiction: itar, license_required, licence_exception, restricted_countries: restricted, embargoed_countries: embargoed, incoterm, notes, classified_at: now_iso(), } } /// Screen a payee + territories against sanctions/DPS lists. fn screen(req: &ScreeningRequest) -> ScreeningResult { let mut blocked: Vec = Vec::new(); let mut review: Vec = Vec::new(); let embargoed: Vec = req .territories .iter() .filter(|t| EMBARGOED_COUNTRIES.contains(&t.as_str())) .cloned() .collect(); let restricted: Vec = req .territories .iter() .filter(|t| RESTRICTED_TERRITORIES.contains(&t.as_str())) .cloned() .collect(); if EMBARGOED_COUNTRIES.contains(&req.payee_country.as_str()) { blocked.push(format!( "Payee country '{}' is under comprehensive embargo", req.payee_country )); } if !embargoed.is_empty() { blocked.push(format!( "Delivery territories under embargo: {}", embargoed.join(", ") )); } if !restricted.is_empty() { review.push(format!( "Restricted territories require manual review: {}", restricted.join(", ") )); } // Large-value payments to high-risk jurisdictions need enhanced due diligence if req.amount_usd > 10_000.0 && restricted.contains(&req.payee_country) { review.push(format!( "Payment >{:.0} USD to restricted territory '{}' — enhanced due diligence required", req.amount_usd, req.payee_country )); } let outcome = if !blocked.is_empty() { ScreeningOutcome::Blocked } else if !review.is_empty() { ScreeningOutcome::ReviewRequired } else { ScreeningOutcome::Clear }; ScreeningResult { screening_id: req.screening_id.clone(), outcome, blocked_reasons: blocked, review_reasons: review, embargoed_territories: embargoed, restricted_territories: restricted, dps_checked: false, // set true when live DPS API called screened_at: now_iso(), } } // ── HTTP handlers ───────────────────────────────────────────────────────────── /// POST /api/gtms/classify pub async fn classify_work( State(state): State, Json(req): Json, ) -> Result, StatusCode> { // LangSec: validate HS code if caller provides one (future override path) let result = classify(&req); if result.license_required { warn!( title=%req.title, embargoed=?result.embargoed_countries, "GTMS: export licence required" ); } state .audit_log .record(&format!( "GTMS_CLASSIFY title='{}' eccn='{}' hs={:?} licence_req={} embargoed={:?}", result.title, result.eccn, result.hs_code, result.license_required, result.embargoed_countries, )) .ok(); state.gtms_db.save_classification(result.clone()); Ok(Json(result)) } /// POST /api/gtms/screen pub async fn screen_distribution( State(state): State, Json(req): Json, ) -> Result, StatusCode> { let result = screen(&req); match result.outcome { ScreeningOutcome::Blocked => { warn!( screening_id=%result.screening_id, payee=%req.payee_name, reasons=?result.blocked_reasons, "GTMS: distribution BLOCKED" ); } ScreeningOutcome::ReviewRequired => { warn!( screening_id=%result.screening_id, payee=%req.payee_name, reasons=?result.review_reasons, "GTMS: distribution requires review" ); } ScreeningOutcome::Clear => { info!(screening_id=%result.screening_id, payee=%req.payee_name, "GTMS: clear"); } } state .audit_log .record(&format!( "GTMS_SCREEN id='{}' payee='{}' outcome={:?} blocked={:?}", result.screening_id, req.payee_name, result.outcome, result.blocked_reasons, )) .ok(); state.gtms_db.save_screening(result.clone()); Ok(Json(result)) } /// GET /api/gtms/declaration/:id pub async fn get_declaration( State(state): State, Path(id): Path, ) -> Result, StatusCode> { state .gtms_db .get_declaration(&id) .map(Json) .ok_or(StatusCode::NOT_FOUND) }