Skip to main content

hs_predict/rules/
chapter38.rs

1//! Chapter 38 — Miscellaneous chemical products.
2//!
3//! Chapter 38 covers **prepared** and **mixed** chemical products that do not
4//! fall neatly into Chapters 28 or 29.  Unlike Chapters 28/29, classification
5//! here depends heavily on **intended use** and **presentation** rather than
6//! chemical structure alone.
7//!
8//! ## Key headings
9//! | Heading | Description |
10//! |---------|-------------|
11//! | 38.02 | Activated carbon; activated natural mineral products |
12//! | 38.09 | Finishing agents, dye carriers for textiles, etc. |
13//! | 38.11 | Anti-knock / anti-oxidant preparations for mineral oils |
14//! | 38.12 | Prepared rubber/plastic stabilizers |
15//! | 38.17 | Mixed alkylbenzenes / alkylnaphthalenes |
16//! | 38.20 | Anti-freezing preparations and de-icing fluids |
17//! | 38.22 | Diagnostic / laboratory reagents |
18//! | 38.23 | Industrial fatty acids, acid oils, industrial fatty alcohols |
19//! | 38.24 | Chemical preparations NEC (catch-all) |
20//!
21//! ## Classification logic
22//! Use [`classify_by_intended_use`] for mixtures whose chapter depends primarily
23//! on end-use.  Use [`CHAPTER38_CATCH_ALL_CODE`] when no other rule applies.
24
25use crate::types::IntendedUse;
26
27// ─────────────────────────────────────────────────────────────────────────────
28// Constants
29// ─────────────────────────────────────────────────────────────────────────────
30
31/// Six-digit HS 2022 catch-all code for chemical preparations NEC (not elsewhere
32/// classified).  Used as the final GRI 3c fallback for mixtures.
33pub(crate) const CHAPTER38_CATCH_ALL_CODE: &str = "382499";
34
35/// Heading description paired with [`CHAPTER38_CATCH_ALL_CODE`].
36pub(crate) const CHAPTER38_CATCH_ALL_DESC: &str =
37    "Chemical preparations, not elsewhere specified or included (Ch. 38 NEC)";
38
39// ─────────────────────────────────────────────────────────────────────────────
40// Intended-use based classification
41// ─────────────────────────────────────────────────────────────────────────────
42
43/// Returns `(hs_code, heading_description, confidence)` for a mixture product
44/// whose HS chapter is determined by its intended use.
45///
46/// Returns `None` when the intended use does not uniquely determine a Chapter 38
47/// heading (e.g. `Industrial` is too broad).
48///
49/// Called by the mixture classifier **before** GRI 3a/3b/3c evaluation.
50pub(crate) fn classify_by_intended_use(
51    intended_use: &IntendedUse,
52) -> Option<(&'static str, &'static str, f32)> {
53    match intended_use {
54        // Agricultural pesticide preparations → 38.08
55        IntendedUse::Agricultural => Some((
56            "380800",
57            "Insecticides, rodenticides, fungicides, herbicides, \
58             anti-sprouting products and plant-growth regulators, \
59             disinfectants and similar products (Ch. 38.08)",
60            0.75,
61        )),
62
63        // Pharmaceutical formulations → Ch. 30
64        // (not Ch.38, handled separately by the pipeline)
65        IntendedUse::Pharmaceutical => None,
66
67        // Cosmetic formulations → Ch. 33
68        // (not Ch.38, handled separately by the pipeline)
69        IntendedUse::Cosmetic => None,
70
71        // Food-grade preparations → Ch. 21
72        // (not Ch.38, handled separately by the pipeline)
73        IntendedUse::Food => None,
74
75        // General industrial use: too broad to determine chapter automatically.
76        IntendedUse::Industrial | IntendedUse::Other(_) => None,
77    }
78}
79
80// ─────────────────────────────────────────────────────────────────────────────
81// Use-case-to-chapter mapping for non-chemical chapters
82// ─────────────────────────────────────────────────────────────────────────────
83
84/// For special-use products that must be classified outside Ch.28/29/38,
85/// returns the chapter they belong to.
86///
87/// Returns `None` when standard pipeline logic applies.
88pub(crate) fn special_chapter_by_use(
89    intended_use: &IntendedUse,
90) -> Option<(&'static str, &'static str, f32)> {
91    match intended_use {
92        IntendedUse::Pharmaceutical => Some((
93            "300490",
94            "Medicaments — other mixtures/preparations for therapeutic use (Ch. 30)",
95            0.70,
96        )),
97        IntendedUse::Cosmetic => Some((
98            "330499",
99            "Beauty/cosmetic preparations — other (Ch. 33)",
100            0.65,
101        )),
102        IntendedUse::Food => Some((
103            "210690",
104            "Food preparations, not elsewhere specified (Ch. 21)",
105            0.65,
106        )),
107        _ => None,
108    }
109}
110
111// ─────────────────────────────────────────────────────────────────────────────
112// Tests
113// ─────────────────────────────────────────────────────────────────────────────
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn agricultural_gives_3808() {
121        let (code, _, confidence) =
122            classify_by_intended_use(&IntendedUse::Agricultural).unwrap();
123        assert_eq!(&code[..2], "38");
124        assert!(confidence > 0.5);
125    }
126
127    #[test]
128    fn pharmaceutical_returns_none_from_ch38_function() {
129        // Pharmaceuticals are handled by special_chapter_by_use, not classify_by_intended_use
130        assert!(classify_by_intended_use(&IntendedUse::Pharmaceutical).is_none());
131    }
132
133    #[test]
134    fn pharmaceutical_gives_ch30_via_special() {
135        let (code, _, _) = special_chapter_by_use(&IntendedUse::Pharmaceutical).unwrap();
136        assert_eq!(&code[..2], "30");
137    }
138
139    #[test]
140    fn cosmetic_gives_ch33_via_special() {
141        let (code, _, _) = special_chapter_by_use(&IntendedUse::Cosmetic).unwrap();
142        assert_eq!(&code[..2], "33");
143    }
144
145    #[test]
146    fn food_gives_ch21_via_special() {
147        let (code, _, _) = special_chapter_by_use(&IntendedUse::Food).unwrap();
148        assert_eq!(&code[..2], "21");
149    }
150
151    #[test]
152    fn industrial_returns_none() {
153        assert!(classify_by_intended_use(&IntendedUse::Industrial).is_none());
154    }
155
156    #[test]
157    fn catch_all_is_6_digits() {
158        assert_eq!(CHAPTER38_CATCH_ALL_CODE.len(), 6);
159        assert!(CHAPTER38_CATCH_ALL_CODE.chars().all(|c| c.is_ascii_digit()));
160    }
161}