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}