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, }