1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//! Textile (EU Textile DPP, expected ~Q2 2027 adoption, compliance ~2028–2029).
use serde::{Deserialize, Serialize};
use crate::identifier::ProductIdentifier;
use super::common::SvhcSubstance;
/// A single fibre entry in a textile product's composition list.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FibreEntry {
/// Fibre name, e.g. `"cotton"`, `"polyester"`, `"recycled_polyester"`.
pub fibre: String,
/// Percentage by weight (0.0–100.0).
///
/// All entries in the composition list must sum to approximately 100.0
/// (± 2.0 percentage point tolerance to accommodate rounding).
pub pct: f64,
/// ISO 3166-1 alpha-2 country code where this specific fibre was sourced.
///
/// Per-fibre origin traceability as anticipated by the textile delegated act.
/// When present, allows downstream actors to verify sourcing per fibre rather
/// than relying on a single `country_of_raw_material_origin` for the whole product.
#[serde(skip_serializing_if = "Option::is_none")]
pub country_of_origin: Option<String>,
}
/// Textile-specific fields for EU textile DPP compliance.
///
/// Based on the anticipated EU Textile DPP delegated act (~Q2 2027 adoption).
/// Fields marked as `Option` are optional under v1.0.0 but may become mandatory
/// once the delegated act is finalised.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TextileData {
// ── Mandatory fields (v1.0.0) ──────────────────────────────────────────
/// The unique product identifier for the textile product, in whichever EN 18219
/// clause 5 scheme issued it — a 14-digit GTIN under scheme 1.
pub product_identifier: ProductIdentifier,
/// List of fibres and their percentage composition. Must sum to ~100%.
pub fibre_composition: Vec<FibreEntry>,
/// ISO 3166-1 alpha-2 country code where the textile was manufactured.
pub country_of_origin: String,
/// ISO 3758 care symbols or free text care instructions.
pub care_instructions: String,
/// Chemical compliance standard, e.g. `"OEKO-TEX 100"`, `"REACH"`, `"GOTS"`.
pub chemical_compliance_standard: String,
// ── Environmental metrics ──────────────────────────────────────────────
/// Total recycled content as a percentage of total weight (0.0–100.0).
#[serde(skip_serializing_if = "Option::is_none")]
pub recycled_content_pct: Option<f64>,
/// Carbon footprint in kg CO₂e per unit.
#[serde(skip_serializing_if = "Option::is_none")]
pub carbon_footprint_kg_co2e: Option<f64>,
/// Water consumption in litres per unit produced.
#[serde(skip_serializing_if = "Option::is_none")]
pub water_use_litres: Option<f64>,
/// Microplastic fibre shedding in milligrams per wash cycle.
///
/// Measured per ISO/DIS 4484 or equivalent. Relevant for synthetic textiles
/// where microplastic release is a growing regulatory concern.
#[serde(skip_serializing_if = "Option::is_none")]
pub microplastic_shedding_mg_per_wash: Option<f64>,
// ── Durability & repairability ─────────────────────────────────────────
/// Repairability score (0.0–10.0) per EU methodology.
#[serde(skip_serializing_if = "Option::is_none")]
pub repair_score: Option<f64>,
/// Durability score (0.0–10.0) measuring resistance to pilling,
/// colour fastness, dimensional stability, and seam strength.
#[serde(skip_serializing_if = "Option::is_none")]
pub durability_score: Option<f64>,
/// Expected number of wash cycles before significant quality degradation.
#[serde(skip_serializing_if = "Option::is_none")]
pub expected_wash_cycles: Option<u32>,
// ── Traceability ───────────────────────────────────────────────────────
/// ISO 3166-1 alpha-2 country of raw material origin (product-level fallback).
/// Per-fibre origin on `FibreEntry.country_of_origin` takes precedence when present.
#[serde(skip_serializing_if = "Option::is_none")]
pub country_of_raw_material_origin: Option<String>,
// ── SCIP / SVHC substance disclosure ───────────────────────────────────
/// List of SVHC substances present above the 0.1% w/w threshold.
///
/// Required by REACH Article 33 and linked to the ECHA SCIP database.
/// An empty vec means the manufacturer has checked and found no SVHCs.
/// `None` means the check has not yet been performed.
#[serde(skip_serializing_if = "Option::is_none")]
pub svhc_substances: Option<Vec<SvhcSubstance>>,
// ── Substances of concern (beyond SVHC) ──────────────────────────────
/// Substances present in the textile that are restricted by **REACH Annex
/// XVII entry 72** — the substances listed in its Appendix 12, in clothing,
/// skin-contact textiles and footwear, at or above the concentration given
/// for each, measured in homogeneous material.
///
/// Appendix 12 is predominantly a **CMR and PAH** list: cadmium, arsenic,
/// lead, benzene and seven polycyclic aromatic hydrocarbons, plus chromium VI
/// and C.I. Disperse Blue 1. Only the last two are skin sensitisers, so the
/// field name is narrower than what it carries — kept because it is the
/// published wire name and renaming it would break stored documents.
///
/// ⚠️ **Nickel does not belong here.** Nickel release from articles in
/// prolonged skin contact is **Annex XVII entry 27**, a separate restriction
/// with its own migration-limit regime. An earlier version of this comment
/// gave "nickel in accessories" as an entry 72 example; it never was one.
/// `v1.1.0` and `v1.2.0` are published and carry that error in their field
/// description — see the errata note in `crate::schemas`.
#[serde(skip_serializing_if = "Option::is_none")]
pub allergens: Option<Vec<String>>,
/// Restricted substances present that are not classified as SVHC but are
/// regulated under product group-specific rules (e.g. PFASs, flame retardants,
/// biocides). Each entry is a free-text substance identifier.
#[serde(skip_serializing_if = "Option::is_none")]
pub substances_of_concern: Option<Vec<String>>,
// ── Circularity & end-of-life ─────────────────────────────────────────
/// Recyclability classification, e.g. `"mono-material"`, `"multi-material"`,
/// `"not-recyclable"`. Based on design-for-recycling assessment.
#[serde(skip_serializing_if = "Option::is_none")]
pub recyclability_class: Option<String>,
/// End-of-life instructions: how to dispose of or recycle the product.
/// Free text or URL pointing to take-back/collection point information.
#[serde(skip_serializing_if = "Option::is_none")]
pub end_of_life_instructions: Option<String>,
/// Whether the product has been refurbished or reused, and if so its
/// condition grade (e.g. `"like-new"`, `"good"`, `"fair"`).
#[serde(skip_serializing_if = "Option::is_none")]
pub reuse_condition: Option<String>,
/// Number of prior ownership cycles (0 for new products).
#[serde(skip_serializing_if = "Option::is_none")]
pub prior_use_cycles: Option<u32>,
// ── Restricted data (gated by access control) ──────────────────────────
/// Free-text or structured disassembly / deconstruction instructions.
/// Classified `Disclosure::Restricted` (Verifiable Credential required).
#[serde(skip_serializing_if = "Option::is_none")]
pub disassembly_instructions: Option<String>,
/// Whether spare parts or replacement components are available from the manufacturer.
#[serde(skip_serializing_if = "Option::is_none")]
pub spare_parts_available: Option<bool>,
/// Product net weight in grams (used for per-unit environmental calculations).
#[serde(skip_serializing_if = "Option::is_none")]
pub product_weight_grams: Option<f64>,
// ── Repair & maintenance history ──────────────────────────────────────
/// URL or structured log of repair events performed on this specific item.
/// Relevant for products sold with a repair history (e.g. certified
/// pre-owned programmes).
#[serde(skip_serializing_if = "Option::is_none")]
pub repair_history_url: Option<String>,
/// Number of completed professional repairs on this item.
#[serde(skip_serializing_if = "Option::is_none")]
pub repair_count: Option<u32>,
/// Environmental impact score per garment lifecycle (PEF/OEF methodology).
/// Dimensionless single score aggregating multiple impact categories.
#[serde(skip_serializing_if = "Option::is_none")]
pub pef_score: Option<f64>,
}
impl crate::product_group::payload::ProductGroupPayload for TextileData {
fn product_identifier(&self) -> Option<&crate::identifier::ProductIdentifier> {
Some(&self.product_identifier)
}
/// This act defines no model identifier.
fn model_identifier(&self) -> Option<&str> {
None
}
/// This group's schema carries a REACH candidate-list declaration.
fn svhc_substances(&self) -> Option<&[crate::product_group::SvhcSubstance]> {
self.svhc_substances.as_deref()
}
/// This group models no product category.
///
/// The catalog declared `apparel`, `footwear` and `home_textile` until
/// they were removed, and no field here ever carried any of them — the
/// textile schema has no category-shaped property at all, at any version.
/// All three were therefore unreachable: nothing could set one and nothing
/// could validate against one. (`home_textile` was also the only snake_case
/// value in the whole catalog, which is the kind of thing an unread list
/// accumulates.)
///
/// The delegated act is unadopted, so what a textile category *is* is not
/// yet readable from a text. Inventing the field ahead of it would put a
/// guess where the act's own axis has to go.
fn product_category(&self) -> Option<&str> {
None
}
}