use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Language {
#[default]
En,
Ja,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SubstanceIdentifier {
pub cas: Option<String>,
pub smiles: Option<String>,
pub iupac_name: Option<String>,
pub inchi: Option<String>,
pub inchi_key: Option<String>,
pub cid: Option<u64>,
}
impl SubstanceIdentifier {
pub fn from_cas(cas: impl Into<String>) -> Self {
Self { cas: Some(cas.into()), ..Default::default() }
}
pub fn from_smiles(smiles: impl Into<String>) -> Self {
Self { smiles: Some(smiles.into()), ..Default::default() }
}
pub fn from_iupac_name(name: impl Into<String>) -> Self {
Self { iupac_name: Some(name.into()), ..Default::default() }
}
pub fn is_empty(&self) -> bool {
self.cas.is_none()
&& self.smiles.is_none()
&& self.iupac_name.is_none()
&& self.inchi.is_none()
&& self.inchi_key.is_none()
&& self.cid.is_none()
}
pub fn display_name(&self) -> String {
if let Some(ref n) = self.iupac_name {
return n.clone();
}
if let Some(ref cas) = self.cas {
return format!("CAS:{}", cas);
}
if let Some(cid) = self.cid {
return format!("CID:{}", cid);
}
if let Some(ref s) = self.smiles {
let short = if s.len() > 20 { &s[..20] } else { s.as_str() };
return format!("SMILES:{}", short);
}
"(unknown)".to_string()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PhysicalForm {
Solid,
Powder {
particle_size_um: Option<f64>,
},
Granules,
Liquid,
Solution {
solvent: Option<String>,
concentration_pct_ww: Option<f64>,
},
Gas,
Foil {
thickness_mm: Option<f64>,
},
Ingot,
Unknown,
}
impl PhysicalForm {
pub fn is_solution(&self) -> bool {
matches!(self, PhysicalForm::Solution { .. })
}
pub fn concentration_pct(&self) -> Option<f64> {
if let PhysicalForm::Solution { concentration_pct_ww, .. } = self {
*concentration_pct_ww
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PurityType {
ReagentGrade,
TechnicalGrade,
PharmaceuticalGrade { standard: Option<String> },
FoodGrade,
ElectronicsGrade,
Specified(f64),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MixtureComponent {
pub substance: SubstanceIdentifier,
pub weight_fraction_pct: Option<f64>,
pub volume_fraction_pct: Option<f64>,
pub is_solvent: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductDescription {
pub identifier: SubstanceIdentifier,
pub physical_form: Option<PhysicalForm>,
pub purity_pct: Option<f64>,
pub purity_type: Option<PurityType>,
pub mixture_components: Option<Vec<MixtureComponent>>,
pub intended_use: Option<IntendedUse>,
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IntendedUse {
Industrial,
Pharmaceutical,
Agricultural,
Food,
Cosmetic,
Other(String),
}
impl ProductDescription {
pub fn is_mixture(&self) -> bool {
self.mixture_components
.as_ref()
.map(|v| !v.is_empty())
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HsPrediction {
pub hs_code: String,
pub heading_description: String,
pub confidence: f32,
pub source: PredictionSource,
pub notes: Vec<String>,
pub alternatives: Vec<AlternativePrediction>,
pub recommended_action: RecommendedAction,
pub jp_tariff_code: Option<String>,
pub jp_tariff_year: Option<u16>,
}
impl HsPrediction {
pub fn chapter(&self) -> &str {
&self.hs_code[..2]
}
pub fn heading(&self) -> &str {
&self.hs_code[..4]
}
pub fn display(&self) -> String {
let c = &self.hs_code;
if c.len() == 6 {
format!("{}.{}.{}", &c[..2], &c[2..4], &c[4..6])
} else {
c.clone()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlternativePrediction {
pub hs_code: String,
pub confidence: f32,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PredictionSource {
UserMapping,
EmbeddedRule { rule_id: String },
RuleEngine { matched_rules: Vec<String> },
LlmApi { model: String },
Hybrid { rule_id: String, model: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecommendedAction {
Accept,
VerifyWithLlm,
ExpertReview,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OrganicInorganic {
Organic,
Inorganic,
Organometallic,
Unknown,
}