File size: 18,630 Bytes
f4dcb85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
"""TariffWise engine. Deterministic walk of the Chapter 64 tree.

The walk descends from a heading to a ten digit leaf. At each node the
children are evaluated in schedule order. A child matches, fails, or needs
a fact. A needed fact stops the walk and names the gap. An unparsed
condition stops the walk for review. The path taken is the rationale.
"""
import json
import re

NODES = json.load(open("tree.json"))
BYID = {n["id"]: n for n in NODES}
ROOTS = [n for n in NODES if n["parent"] is None]

# ---------------------------------------------------------------------
# Fact schema. Every key the schedule's splits can turn on.
# ---------------------------------------------------------------------
FACT_KEYS = {
    "sole_material":      "outer sole constituent material, rubber_plastics, leather, composition_leather, or other",
    "upper_material":     "upper constituent material by greatest external surface area, rubber_plastics, leather, composition_leather, textile, or other",
    "upper_textile_kind": "when the upper is textile, vegetable_fibers, wool_felt, or other_textile",
    "waterproof_molded":  "true when soles and uppers are rubber or plastics and the upper is neither fixed to the sole nor assembled by stitching, riveting, nailing, screwing, plugging or similar",
    "protective":         "true when the footwear protects against water, oil, grease, chemicals, cold or inclement weather",
    "metal_toe_cap":      "true when a protective metal toe cap is present",
    "covers_ankle":       "true when the footwear covers the ankle",
    "covers_knee":        "true when the footwear covers the knee",
    "sports_spikes":      "true when designed for a sport with spikes, sprigs, stops, clips, bars or provision for them, or ski, skating, snowboard, wrestling, boxing or cycling boots",
    "ski_boot":           "true for ski boots, cross country ski footwear and snowboard boots",
    "athletic_like":      "true for tennis shoes, basketball shoes, gym shoes, training shoes and the like",
    "leather_esa_over_50":"true when over 50 percent of the upper external surface area is leather including accessories added back",
    "value_per_pair":     "customs value in US dollars per pair",
    "gender_age":         "men, women, youths_boys, misses, children, infants, or unisex",
    "house_slipper":      "true for house slippers",
    "work_footwear":      "true for work footwear",
    "welt":               "true for welt construction footwear",
    "zori":               "true for zoris, one piece molded rubber or plastic thong sandals",
    "upper_esa_rubber_plastics_over_90": "true when over 90 percent of the upper external surface area including accessories is rubber or plastics",
    "foxing_band":        "true when the footwear has a foxing or foxing like band applied or molded at the sole and overlapping the upper",
    "open_toe_or_heel":   "true when the footwear has an open toe or open heel",
    "slip_on":            "true when the footwear is of the slip on type without laces, buckles or fasteners",
    "sole_attach_stitch": "true when the sole is affixed to the upper mainly by stitching",
    "vulcanized_construction": "true for footwear made by vulcanization or similar one piece processes",
    "textile_sole_contact":"true when textile material has the greatest surface area in contact with the ground",
    "leather_strap_instep_big_toe": "true when the upper consists of leather straps across the instep and around the big toe",
    "wood_platform": "true when the footwear is made on a base or platform of wood",
    "inner_sole": "true when the footwear has an inner sole",
    "rp_weight_under_10": "true when the footwear is less than 10 percent by weight of rubber and plastics",
    "turned_construction": "true for turn or turned footwear, sewn wrong side out to a leather sole then turned",
    "golf_shoes": "true for golf shoes",
}

# ---------------------------------------------------------------------
# Condition matchers. Each returns True, False, or a needed fact key.
# UNPARSED marks text no rule recognizes.
# ---------------------------------------------------------------------
UNPARSED = "UNPARSED"

def need(facts, key):
    return facts[key] if key in facts else key

COMPOUND_MARKERS = ("which consist", "straps", "provided", "except as", "in which")

def match_condition(desc, facts):
    ov = facts.get("_overrides", {})
    if desc in ov:
        return bool(ov[desc])
    d = desc.lower().rstrip(":").strip()

    # residual buckets always match once earlier siblings failed
    if d in ("other", "other footwear", "other:"):
        return True

    # leather strap sandal type of 6403.20
    if "leather straps across the instep and around the big toe" in d:
        t = need(facts, "leather_strap_instep_big_toe")
        return t if t == "leather_strap_instep_big_toe" else bool(facts["leather_strap_instep_big_toe"])

    # sole grouping rows under 6403 and 6404
    if d.startswith("footwear with outer soles of rubber or plastics"):
        s = need(facts, "sole_material")
        return s if s == "sole_material" else facts["sole_material"] == "rubber_plastics"
    if d.startswith("footwear with outer soles of leather or composition leather"):
        s = need(facts, "sole_material")
        return s if s == "sole_material" else facts["sole_material"] in ("leather","composition_leather")

    # open toe or heel and slip on lines, honoring the except clauses
    if d.startswith("footwear with open toes or open heels"):
        if "foxing" in d:
            f = need(facts, "foxing_band")
            if f == "foxing_band": return f
            if facts["foxing_band"]: return False
        if "6404.19.20" in d:
            p = need(facts, "protective")
            if p == "protective": return p
            if facts["protective"]: return False
        o = need(facts, "open_toe_or_heel")
        if o == "open_toe_or_heel": return o
        if facts["open_toe_or_heel"]: return True
        if "slip-on" in d or "slip on" in d:
            sl = need(facts, "slip_on")
            return sl if sl == "slip_on" else bool(facts["slip_on"])
        return False

    # weight of rubber or plastics split under 6404.19
    if "less than 10 percent by weight of rubber or plastics" in d:
        w = need(facts, "rp_weight_under_10")
        return w if w == "rp_weight_under_10" else bool(facts["rp_weight_under_10"])
    if "10 percent or more by weight of rubber or plastics" in d:
        w = need(facts, "rp_weight_under_10")
        return w if w == "rp_weight_under_10" else not facts["rp_weight_under_10"]

    # turned construction and golf shoes
    if d.startswith("turn or turned footwear"):
        t = need(facts, "turned_construction")
        return t if t == "turned_construction" else bool(facts["turned_construction"])
    if d == "golf shoes":
        g = need(facts, "golf_shoes")
        return g if g == "golf_shoes" else bool(facts["golf_shoes"])

    # compound conditions no simple rule should decide
    if any(m in d for m in COMPOUND_MARKERS):
        return UNPARSED

    # value bands
    m = re.search(r"valued not over \$?([\d.]+)", d)
    if m:
        v = need(facts, "value_per_pair")
        if v == "value_per_pair": return v
        return float(facts["value_per_pair"]) <= float(m.group(1))
    m = re.search(r"valued over \$?([\d.]+)(?:/pair)? but not over \$?([\d.]+)", d)
    if m:
        v = need(facts, "value_per_pair")
        if v == "value_per_pair": return v
        return float(m.group(1)) < float(facts["value_per_pair"]) <= float(m.group(2))
    m = re.search(r"valued over \$?([\d.]+)", d)
    if m:
        v = need(facts, "value_per_pair")
        if v == "value_per_pair": return v
        return float(facts["value_per_pair"]) > float(m.group(1))

    # gender and age lines
    g = need(facts, "gender_age")
    gmap = {
        "for men": "men", "men's": "men",
        "for women": "women", "women's": "women",
        "for youths and boys": "youths_boys",
        "for misses": "misses", "for children": "children", "for infants": "infants",
        "for men, youths and boys": ("men","youths_boys"),
        "for other persons": None,
        "for children and infants": ("children","infants"),
        "for misses, children and infants": ("misses","children","infants"),
    }
    for text, val in gmap.items():
        if d == text or d.startswith(text + " "):
            if g == "gender_age": return g
            if val is None:
                return facts["gender_age"] not in ("men","youths_boys")
            if isinstance(val, tuple):
                return facts["gender_age"] in val
            return facts["gender_age"] == val

    # ankle and knee
    if "covering the ankle but not covering the knee" in d:
        a = need(facts, "covers_ankle")
        if a == "covers_ankle": return a
        if not facts["covers_ankle"]: return False
        k = need(facts, "covers_knee")
        return k if k == "covers_knee" else not facts["covers_knee"]
    if "not covering the knee" not in d and "covering the knee" in d:
        k = need(facts, "covers_knee")
        return k if k == "covers_knee" else bool(facts["covers_knee"])
    if "not covering the ankle" in d:
        a = need(facts, "covers_ankle")
        return a if a == "covers_ankle" else not facts["covers_ankle"]
    if "covering the ankle" in d:
        a = need(facts, "covers_ankle")
        if a == "covers_ankle": return a
        if not facts["covers_ankle"]: return False
        if "but not covering the knee" in d:
            k = need(facts, "covers_knee")
            return k if k == "covers_knee" else not facts["covers_knee"]
        return True

    # sports and athletic
    if d.startswith("sports footwear"):
        s = need(facts, "sports_spikes")
        if s == "sports_spikes": return s
        if facts["sports_spikes"]: return True
        if "tennis shoes, basketball shoes, gym shoes" in d:
            a = need(facts, "athletic_like")
            return a if a == "athletic_like" else bool(facts["athletic_like"])
        return False
    if "ski-boots" in d or "ski boots" in d or "snowboard" in d:
        s = need(facts, "ski_boot")
        return s if s == "ski_boot" else bool(facts["ski_boot"])
    if "tennis shoes, basketball shoes, gym shoes" in d:
        a = need(facts, "athletic_like")
        return a if a == "athletic_like" else bool(facts["athletic_like"])

    # wood platform line of 6403.59.10
    if "base or platform of wood" in d:
        w = need(facts, "wood_platform")
        if w == "wood_platform": return w
        if not facts["wood_platform"]: return False
        t = need(facts, "metal_toe_cap")
        if t == "metal_toe_cap": return t
        if facts["metal_toe_cap"]: return False
        i = need(facts, "inner_sole")
        return i if i == "inner_sole" else not facts["inner_sole"]

    # toe cap and protection, only when the line is about the toe cap itself
    if ("protective metal toe-cap" in d or "protective metal toe cap" in d) and d.startswith(("incorporating", "other footwear, incorporating", "footwear incorporating", "not incorporating")):
        t = need(facts, "metal_toe_cap")
        if t == "metal_toe_cap": return t
        if "not " in d.split("incorporating")[0]:
            return not facts["metal_toe_cap"]
        return bool(facts["metal_toe_cap"])
    if "protection against water, oil, grease or chemicals or cold or inclement weather" in d:
        p = need(facts, "protective")
        if p == "protective": return p
        if d.startswith("footwear designed to be worn over"):
            return bool(facts["protective"])
        return bool(facts["protective"])
    if d.startswith("other footwear, not designed to be") or "not designed to be worn over" in d:
        p = need(facts, "protective")
        return p if p == "protective" else not facts["protective"]

    # upper leather share
    if "over 50 percent of the external surface area" in d and "leather" in d:
        l = need(facts, "leather_esa_over_50")
        return l if l == "leather_esa_over_50" else bool(facts["leather_esa_over_50"])

    # rubber plastics share over 90
    if "over 90 percent of the external surface area" in d:
        r = need(facts, "upper_esa_rubber_plastics_over_90")
        if r == "upper_esa_rubber_plastics_over_90": return r
        val = bool(facts["upper_esa_rubber_plastics_over_90"])
        if "foxing" in d:
            f = need(facts, "foxing_band")
            if f == "foxing_band": return f
            return val and not facts["foxing_band"]
        return val

    # upper material lines
    if "uppers of vegetable fibers" in d or "of vegetable fibers" in d:
        u = need(facts, "upper_textile_kind")
        return u if u == "upper_textile_kind" else facts["upper_textile_kind"] == "vegetable_fibers"
    if "soles and uppers of wool felt" in d:
        u = need(facts, "upper_textile_kind")
        return u if u == "upper_textile_kind" else facts["upper_textile_kind"] == "wool_felt"
    if "uppers of leather or composition leather" in d:
        u = need(facts, "upper_material")
        return u if u == "upper_material" else facts["upper_material"] in ("leather","composition_leather")
    if "uppers of textile materials" in d or "uppers of textile material" in d:
        u = need(facts, "upper_material")
        return u if u == "upper_material" else facts["upper_material"] == "textile"
    if d.startswith("of leather"):
        u = need(facts, "upper_material")
        return u if u == "upper_material" else facts["upper_material"] == "leather"
    if d.startswith("of textile materials"):
        u = need(facts, "upper_material")
        return u if u == "upper_material" else facts["upper_material"] == "textile"

    # sole lines
    if "outer soles of leather" in d:
        s = need(facts, "sole_material")
        return s if s == "sole_material" else facts["sole_material"] == "leather"
    if "outer soles of rubber, plastics, leather or composition leather" in d:
        return True
    if "outer soles and uppers of rubber or plastics" in d:
        s = need(facts, "sole_material"); u = need(facts, "upper_material")
        if s == "sole_material": return s
        if u == "upper_material": return u
        return facts["sole_material"] == "rubber_plastics" and facts["upper_material"] == "rubber_plastics"

    # construction and type lines
    if "welt footwear" in d:
        w = need(facts, "welt")
        return w if w == "welt" else bool(facts["welt"])
    if "house slippers" in d:
        h = need(facts, "house_slipper")
        return h if h == "house_slipper" else bool(facts["house_slipper"])
    if "work footwear" in d:
        w = need(facts, "work_footwear")
        return w if w == "work_footwear" else bool(facts["work_footwear"])
    if "zoris" in d:
        z = need(facts, "zori")
        return z if z == "zori" else bool(facts["zori"])

    return UNPARSED

# ---------------------------------------------------------------------
# The walk.
# ---------------------------------------------------------------------
def walk(facts, start=None):
    """Walk to a leaf. Returns a dict with status, code or gap, and the path."""
    path = []

    def effective_rate(node):
        cur = node
        while cur is not None:
            if cur["general"].strip():
                return cur["general"].strip()
            cur = BYID[cur["parent"]] if cur["parent"] is not None else None
        return ""

    def descend(node):
        if not node["children"]:
            return {"status": "code", "code": node["htsno"], "general": effective_rate(node), "path": path}
        kids = [BYID[c] for c in node["children"]]
        for kid in kids:
            r = match_condition(kid["desc"], facts)
            if r is True:
                path.append({"htsno": kid["htsno"], "desc": kid["desc"]})
                return descend(kid)
            if r is False:
                continue
            if r == UNPARSED:
                return {"status": "review", "reason": "condition needs judgment",
                        "at": kid["desc"], "path": path}
            return {"status": "need_fact", "fact": r, "question_about": FACT_KEYS[r],
                    "at": kid["desc"], "path": path}
        return {"status": "review", "reason": "no child matched", "at": node["desc"], "path": path}

    if start is None:
        # heading selection from the top
        for root in ROOTS:
            r = heading_match(root, facts)
            if r is True:
                path.append({"htsno": root["htsno"], "desc": root["desc"]})
                return descend(root)
            if r is False:
                continue
            if isinstance(r, str):
                return {"status": "need_fact", "fact": r, "question_about": FACT_KEYS[r],
                        "at": root["desc"], "path": path}
        return {"status": "review", "reason": "no heading matched", "path": path}
    return descend(start)

def heading_match(root, facts):
    h = root["htsno"]
    s = facts.get("sole_material"); u = facts.get("upper_material")
    if h == "6401":
        if s is not None and s != "rubber_plastics": return False
        if u is not None and u != "rubber_plastics": return False
        for k in ("sole_material","upper_material","waterproof_molded"):
            if k not in facts: return k
        return bool(facts["waterproof_molded"])
    if h == "6402":
        if s is not None and s != "rubber_plastics": return False
        if u is not None and u != "rubber_plastics": return False
        for k in ("sole_material","upper_material"):
            if k not in facts: return k
        if "waterproof_molded" not in facts: return "waterproof_molded"
        return not facts["waterproof_molded"]
    if h == "6403":
        for k in ("sole_material","upper_material"):
            if k not in facts: return k
        return (s in ("rubber_plastics","leather","composition_leather")
                and u in ("leather",))
    if h == "6404":
        for k in ("sole_material","upper_material"):
            if k not in facts: return k
        return (s in ("rubber_plastics","leather","composition_leather")
                and u == "textile")
    if h == "6405":
        return True
    return False

def rationale(result, facts):
    lines = []
    for i, step in enumerate(result.get("path", []), 1):
        num = step["htsno"] or "grouping"
        lines.append(f"{i}. {num}  {step['desc']}")
    return lines

if __name__ == "__main__":
    print("engine loaded", len(NODES), "nodes")