File size: 5,657 Bytes
b233cf7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

import logging
from typing import Any

from .models import Formula, Ingredient

logger = logging.getLogger("pino.ifra")


# Embedded IFRA Category 4 (fine fragrance) restriction table.
# Values are the maximum finished-product concentration in % (w/w).
# Restricted materials only; most common materials are unrestricted.
IFRA_RESTRICTIONS: dict[str, dict[str, Any]] = {
    "64-17-5": {"material": "Ethanol", "category_4_pct": 100.0, "note": "unrestricted"},
    "5989-27-5": {"material": "D-Limonene", "category_4_pct": 33.0, "note": "sensitization"},
    "138-86-3": {"material": "Limonene", "category_4_pct": 33.0, "note": "sensitization"},
    "97-53-0": {"material": "Eugenol", "category_4_pct": 6.5, "note": "sensitization"},
    "97-54-1": {"material": "Isoeugenol", "category_4_pct": 2.0, "note": "sensitization"},
    "80-54-6": {"material": "Lilial", "category_4_pct": 0.0, "note": "prohibited"},
    "122-40-7": {"material": "Alpha-amyl cinnamic aldehyde", "category_4_pct": 5.8, "note": "sensitization"},
    "101-86-0": {"material": "Alpha-hexyl cinnamic aldehyde", "category_4_pct": 6.6, "note": "sensitization"},
    "104-55-2": {"material": "Cinnamaldehyde", "category_4_pct": 4.0, "note": "sensitization"},
    "143-50-0": {"material": "Methyl eugenol", "category_4_pct": 0.02, "note": "systemic toxicity"},
    "92-48-8": {"material": "6-Methylcoumarin", "category_4_pct": 0.0, "note": "prohibited"},
    "91-64-5": {"material": "Coumarin", "category_4_pct": 1.5, "note": "hepatotoxicity"},
    "121-33-5": {"material": "Vanillin", "category_4_pct": 25.0, "note": "sensitization"},
    "8016-38-4": {"material": "Bergamot oil", "category_4_pct": 4.0, "note": "phototoxicity"},
    "8007-75-8": {"material": "Lime oil", "category_4_pct": 4.0, "note": "phototoxicity"},
    "8008-56-8": {"material": "Lemon oil", "category_4_pct": 12.0, "note": "phototoxicity"},
    "8000-27-9": {"material": "Bitter orange oil", "category_4_pct": 12.0, "note": "phototoxicity"},
    "8000-48-4": {"material": "Grapefruit oil", "category_4_pct": 10.0, "note": "phototoxicity"},
    "8008-31-9": {"material": "Mandarin oil", "category_4_pct": 20.0, "note": "phototoxicity"},
    "9000-05-9": {"material": "Peru balsam", "category_4_pct": 2.0, "note": "sensitization"},
    "9000-50-4": {"material": "Styrax resinoid", "category_4_pct": 2.0, "note": "sensitization"},
    "8014-09-3": {"material": "Ylang-ylang oil", "category_4_pct": 7.5, "note": "sensitization"},
    "8000-34-8": {"material": "Clove oil", "category_4_pct": 5.0, "note": "sensitization"},
    "8006-84-6": {"material": "Cinnamon bark oil", "category_4_pct": 4.0, "note": "sensitization"},
    "8007-08-7": {"material": "Cinnamon leaf oil", "category_4_pct": 4.0, "note": "sensitization"},
    "9000-64-0": {"material": "Oakmoss absolute", "category_4_pct": 0.6, "note": "sensitization"},
    "68606-12-6": {"material": "Treemoss absolute", "category_4_pct": 0.6, "note": "sensitization"},
    "119-36-8": {"material": "Methyl salicylate", "category_4_pct": 4.0, "note": "systemic toxicity"},
    "89-68-9": {"material": "p-Chloro-m-cresol", "category_4_pct": 0.0, "note": "prohibited"},
    "83-66-9": {"material": "Musk ambrette", "category_4_pct": 0.0, "note": "prohibited"},
    "81-14-1": {"material": "Musk ketone", "category_4_pct": 4.0, "note": "systemic toxicity"},
    "105-95-3": {"material": "Ethylene brassylate", "category_4_pct": 20.0, "note": "sensitization"},
    "81-15-2": {"material": "Musk xylene", "category_4_pct": 0.0, "note": "prohibited"},
    "131-11-3": {"material": "Dimethyl phthalate", "category_4_pct": 5.0, "note": "systemic toxicity"},
    "84-66-2": {"material": "Diethyl phthalate", "category_4_pct": 5.0, "note": "systemic toxicity"},
    "25265-71-8": {"material": "Dipropylene glycol", "category_4_pct": 100.0, "note": "unrestricted"},
    "120-51-4": {"material": "Benzyl benzoate", "category_4_pct": 35.0, "note": "unrestricted"},
}


def check_ifra_restrictions(formula: Formula) -> dict[str, Any]:
    """
    Check a formula against the embedded IFRA Category 4 restriction table.

    Returns a dict with 'passed', 'violations', and 'max_usage'.
    """
    violations: list[dict[str, Any]] = []
    max_usage: list[dict[str, Any]] = []
    weights = formula.weight_fractions
    passed = True

    for ing, wf in zip(formula.ingredients, weights):
        cas = ing.cas
        if cas not in IFRA_RESTRICTIONS:
            continue

        restriction = IFRA_RESTRICTIONS[cas]
        limit = restriction["category_4_pct"]
        pct = wf * 100.0

        max_usage.append(
            {
                "cas": cas,
                "material": restriction["material"],
                "used_pct": pct,
                "limit_pct": limit,
            }
        )

        if limit == 0.0 and pct > 0.0:
            violations.append(
                {
                    "cas": cas,
                    "material": restriction["material"],
                    "used_pct": pct,
                    "limit_pct": limit,
                    "reason": restriction["note"],
                }
            )
            passed = False
        elif limit > 0.0 and pct > limit:
            violations.append(
                {
                    "cas": cas,
                    "material": restriction["material"],
                    "used_pct": pct,
                    "limit_pct": limit,
                    "reason": restriction["note"],
                }
            )
            passed = False

    return {
        "passed": passed,
        "violations": violations,
        "max_usage": max_usage,
    }