#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
销售下单页后端 —— 三合一（货号 · 历史成交价 · 特殊要求）+ 审批规则引擎

单据落盘：数据/orders_placed/<id>.json + _index.json
审批规则（用户 2026-09-22 确认）：
  ① 账期：优先看客户已记录账期（特殊要求/ERP 提取，归一化天数）；无记录→按**该销售员的授权档位**
     （授权表逐个销售配，未配＝默认基线 预付款/7个工作日）。本单账期 > 授权 → 审批【账期超额】
  ② 价格：本单单价 ≠ 该客户该货号【该单位族最近一次成交价】→ 审批【价格调整】（改价一律上报）
  ③ 运输：含 PF 开头货号 且（空运 或 货拉拉）→ 审批【PF特殊运输】；承运商＝货拉拉 → 审批【货拉拉发货】
  ④ 触发任一审批 → 必须填写「特殊发货渠道/超授权原因」，随审批内容上报
路由：由 销售工作台服务.py 调用 handle_get / handle_post
"""
import json, os, re, io
from datetime import datetime

BASE = os.path.expanduser('~/Desktop/Hermes输出-工作类')
DATA = os.path.join(BASE, '数据')
ORDER_DIR = os.path.join(DATA, 'orders_placed')
AUTHZ_FILE = os.path.join(DATA, 'order_authorization.json')
SHIP_FILE = os.path.join(DATA, 'customer_shipping.json')
SEQ_FILE = os.path.join(DATA, 'order_contract_seq.json')

DEPS = {'visible_sales': None, 'notify': None}
_CACHE = {}
APPROVER_DEFAULT = '刘新元'

TERMS_DAYS = {'预付款': 0.0, '7个工作日': 7.0, '1个月': 30.0, '2个月': 60.0, '其他': 999.0}
DEFAULT_TERMS_LABEL = '7个工作日'
SPEC_FAMILY = {}
for _t in ('100T', '1000T', '10000T'):
    SPEC_FAMILY[_t] = 'T'
for _v in ('0.1ml', '1ml', '5ml', '10ml', '40ml', '100ml'):
    SPEC_FAMILY[_v] = 'ml'
CARRIERS = ['顺丰', '京东', '货拉拉']
MODES = ['陆运', '空运']


def setup(visible_sales=None, notify=None):
    DEPS['visible_sales'] = visible_sales
    DEPS['notify'] = notify


# ───────────── 基础读写（带 mtime 缓存，避免每次请求重读几 MB） ─────────────
def _load(path, default=None):
    try:
        mt = os.path.getmtime(path)
    except OSError:
        return default
    c = _CACHE.get(path)
    if c and c[0] == mt:
        return c[1]
    try:
        with io.open(path, encoding='utf-8') as f:
            d = json.load(f)
    except Exception:
        d = default
    _CACHE[path] = (mt, d)
    return d


def _dump(path, obj):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    tmp = path + '.tmp'
    with io.open(tmp, 'w', encoding='utf-8') as f:
        json.dump(obj, f, ensure_ascii=False, indent=1)
    os.replace(tmp, path)


def sku_index():
    return (_load(os.path.join(DATA, 'order_sku_index.json'), {}) or {}).get('skus', {})


def price_hist():
    return (_load(os.path.join(DATA, 'customer_price_history.json'), {}) or {}).get('hist', {})


def cust_ctx():
    return (_load(os.path.join(DATA, 'customer_order_context.json'), {}) or {}).get('ctx', {})


# ───────────── 授权表 ─────────────
def authz():
    d = _load(AUTHZ_FILE)
    if not d:
        d = default_authz()
    return d


def default_authz():
    ctx = cust_ctx()
    sales = {}
    for cust, v in ctx.items():
        sp = v.get('sp')
        if sp and sp not in sales:
            sales[sp] = {'dept': v.get('dept', ''), 'line': v.get('line', ''), 'customers': 0,
                         'terms_label': DEFAULT_TERMS_LABEL, 'terms_days': TERMS_DAYS[DEFAULT_TERMS_LABEL],
                         'price_change': False, 'special_shipping': False,
                         'configured': False, 'note': '', 'updated_at': '', 'updated_by': ''}
        if sp:
            sales[sp]['customers'] = sales[sp].get('customers', 0) + 1
    return {
        'version': 1,
        'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
        'updated_by': '',
        'approvers': [APPROVER_DEFAULT],
        'defaults': {'terms_label': DEFAULT_TERMS_LABEL, 'terms_days': TERMS_DAYS[DEFAULT_TERMS_LABEL],
                     'price_change': False, 'special_shipping': False,
                     'note': '未单独配置的销售员一律按此基线：预付款 / 7个工作日自主，1个月及以上需审批'},
        'sales': sales,
        'customers': {},
        'terms_options': ['预付款', '7个工作日', '1个月', '2个月', '其他'],
    }


def save_authz(patch, user):
    a = authz()
    if 'sales' in patch and isinstance(patch['sales'], dict):
        for name, cfg in patch['sales'].items():
            cur = a['sales'].get(name) or {}
            cur.update(cfg or {})
            lbl = cur.get('terms_label') or DEFAULT_TERMS_LABEL
            if lbl not in TERMS_DAYS:
                lbl = DEFAULT_TERMS_LABEL
            cur['terms_label'] = lbl
            cur['terms_days'] = TERMS_DAYS[lbl]
            cur['configured'] = True
            cur['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            cur['updated_by'] = user.get('name', '')
            a['sales'][name] = cur
    if 'customers' in patch and isinstance(patch['customers'], dict):
        for name, cfg in patch['customers'].items():
            cur = a.setdefault('customers', {}).get(name) or {}
            cur.update(cfg or {})
            cur['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            cur['updated_by'] = user.get('name', '')
            a['customers'][name] = cur
    if 'defaults' in patch and isinstance(patch['defaults'], dict):
        a.setdefault('defaults', {}).update(patch['defaults'])
    if 'approvers' in patch and isinstance(patch['approvers'], list):
        a['approvers'] = [x for x in patch['approvers'] if x]
    a['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    a['updated_by'] = user.get('name', '')
    _dump(AUTHZ_FILE, a)
    return a


def is_approver(user, a=None):
    a = a or authz()
    if (user.get('role') or '') in ('admin', 'gm'):
        return True
    return (user.get('name') or '') in (a.get('approvers') or [])


def granted_days(customer, sales_name, a=None):
    """→ (授权天数, 档位文案, 依据来源)  客户已记录账期优先，其次该销售员授权档位"""
    a = a or authz()
    c = (a.get('customers') or {}).get(customer) or {}
    if c.get('terms_label'):
        lbl = c['terms_label']
        return TERMS_DAYS.get(lbl, 999.0), lbl, '客户专项授权（%s）' % (c.get('updated_by') or '人工')
    ctx = cust_ctx().get(customer) or {}
    if ctx.get('terms_days') is not None:
        return float(ctx['terms_days']), ctx.get('terms_granted') or '', \
               '客户已记录账期：%s' % ((ctx.get('terms') or [{}])[0].get('raw', '')[:40] or '特殊要求库')
    s = (a.get('sales') or {}).get(sales_name) or {}
    lbl = s.get('terms_label') or (a.get('defaults') or {}).get('terms_label') or DEFAULT_TERMS_LABEL
    src = '销售员授权档位' if s.get('configured') else '默认基线（该销售员未单独授权）'
    return TERMS_DAYS.get(lbl, 7.0), lbl, src


# ───────────── 合同编号流水 ─────────────
def next_contract_no(customer_cc='', bump=False):
    seq = _load(SEQ_FILE, {}) or {}
    day = datetime.now().strftime('%Y%m%d')
    n = int(seq.get(day, 0)) + 1
    if bump:
        seq[day] = n
        for k in list(seq):
            if k < (datetime.now().strftime('%Y%m%d')[:6]) + '01':
                seq.pop(k, None)
        _dump(SEQ_FILE, seq)
    cc = re.sub(r'[^\w\u4e00-\u9fa5\-]', '', (customer_cc or '').strip())[:32]
    return ('%s-' % cc if cc else '') + 'ZHBR-%s-%03d' % (day, n)


# ───────────── 价格 / 规格 ─────────────
def price_info(customer, sku, spec=''):
    h = (price_hist().get(customer) or {}).get(sku) or {}
    fams = h.get('fams') or {}
    fam = SPEC_FAMILY.get(spec or '', '')
    pick = fams.get(fam) if fam else None
    return {'fam': fam, 'fams': fams, 'pick': pick, 'units': h.get('units') or []}


# ───────────── 套件构成（父项↔子项，来自产品目录 BOM）─────────────
def bom_map():
    return _load(os.path.join(DATA, 'product_bom_map.json'), {}) or {}


def latest_mc(fams):
    """取成交最新那一族的物料编码 —— 用于把「货号」升格为「货号+编码」精准匹配"""
    best, mc = '', ''
    for _fam, f in (fams or {}).items():
        d = (f.get('last') or {})
        if (d.get('d') or '') >= best:
            best, mc = d.get('d') or '', d.get('mc') or ''
    return mc


def bom_info(sku, mc=''):
    """该货号是套件父项 → 返回子项构成；是组分 → 返回所属套件。

    ⚠️ 两条容易踩的坑：
    ① 货号不是主键 —— 同一货号对应多规格多物料编码（FM2181-4-NA 有 7 个），
       每个规格的子项/用量都不同，所以优先 `货号||物料编码` 精准匹配。
    ② **父项侧与子项侧的命中必须分开判断**：客户的物料编码可能是「子项编码」
       （组分直售），此时只该显示「属于哪些套件」，
       绝不能因为子项侧命中了就退回去展示父项侧「全规格合并」的子项清单 —— 会串规格误导人。
    """
    b = bom_map()
    if not sku or not b:
        return None
    ck, pk = b.get('children_of_key') or {}, b.get('parents_of_key') or {}
    cg, pg = b.get('children_of') or {}, b.get('parents_of') or {}
    key = ('%s||%s' % (sku, mc)) if mc else ''
    ex_k = bool(key and key in ck)
    ex_p = bool(key and key in pk)
    kids = ck.get(key) if ex_k else (cg.get(sku) or [])
    pars = pk.get(key) if ex_p else (pg.get(sku) or [])
    if not kids and not pars:
        return None
    return {'mc': mc,
            'exact_kids': ex_k, 'exact_parents': ex_p,
            'kids_specs': len({k.get('pc') or '' for k in kids if k.get('pc')}),
            'children': kids, 'parents': pars}


def bom_brief(sku, mc=''):
    """清单页用的轻量标记：该货号是不是套件 / 是不是组分"""
    bi = bom_info(sku, mc)
    if not bi:
        return None
    return {'n': len(bi['children']), 'par': len(bi['parents']),
            'exact': bi['exact_kids'], 'specs': bi['kids_specs']}


def eval_approvals(order, user, a=None):
    """规则引擎 → 审批项列表（每项含 type/detail，供审批人判断）"""
    a = a or authz()
    items = []
    cust = order.get('customer') or ''
    # ① 账期
    td = float(order.get('terms_days') or 0)
    gd, gl, gsrc = granted_days(cust, order.get('sales') or user.get('name', ''), a)
    if td > gd:
        items.append({
            'type': '账期超额',
            'detail': '本单账期【%s】超出授权【%s】（%s）；超出 %s 天'
                      % (order.get('terms') or '', gl or '无', gsrc,
                         int(td - gd) if td < 900 and gd < 900 else '∞'),
            'level': 'high'})
    # ② 价格改动
    px_changes, no_base = [], []
    for ln in order.get('lines') or []:
        pi = price_info(cust, ln.get('sku') or '', ln.get('spec') or '')
        base = (pi.get('pick') or {}).get('last') or None
        try:
            newp = float(ln.get('price') or 0)
        except (TypeError, ValueError):
            newp = 0.0
        if base and float(base.get('p') or 0) > 0:
            bp = float(base['p'])
            if abs(newp - bp) > max(1e-9, bp * 1e-6):
                pct = (newp - bp) / bp * 100.0
                px_changes.append('%s：%s → %s 元/%s（%+.1f%%，原单 %s %s）'
                                  % (ln.get('sku'), bp, newp, ln.get('unit') or base.get('u') or '',
                                     pct, base.get('no'), base.get('d')))
        else:
            no_base.append(ln.get('sku'))
    if px_changes:
        items.append({'type': '价格调整', 'detail': '对历史成交价有修改 —— ' + '；'.join(px_changes), 'level': 'high'})
    if no_base:
        items.append({'type': '无历史成交价', 'detail': '以下货号在该客户无成交记录，属首次定价（不触发审批，建议注明定价依据）：%s'
                      % '、'.join(no_base), 'level': 'info'})
    # ③ 运输
    pf = [ln.get('sku') for ln in (order.get('lines') or []) if str(ln.get('sku') or '').upper().startswith('PF')]
    carrier, mode = order.get('carrier') or '', order.get('transport') or ''
    if pf and (mode == '空运' or carrier == '货拉拉'):
        items.append({'type': 'PF特殊运输',
                      'detail': 'PF（预冻干）货号 %d 个（%s）走【%s/%s】，需特殊发货渠道审批'
                                % (len(pf), '、'.join(pf[:3]) + ('…' if len(pf) > 3 else ''), carrier, mode),
                      'level': 'high'})
    if carrier == '货拉拉':
        items.append({'type': '货拉拉发货', 'detail': '承运商选择【货拉拉】，一律需审批', 'level': 'high'})
    return items


def need_reason(items):
    return any(x.get('level') == 'high' for x in items)


# ───────────── 飞书文本 ─────────────
def feishu_text(o):
    L = []
    L.append('【下单】%s' % (o.get('id') or ''))
    L.append('【客户】%s' % (o.get('customer') or ''))
    if o.get('contact') or o.get('phone'):
        L.append('【收件】%s %s' % (o.get('contact') or '', o.get('phone') or ''))
    if o.get('address'):
        L.append('【地址】%s' % o.get('address'))
    L.append('【合同编号】%s' % (o.get('contract') or ''))
    if o.get('customer_cc'):
        L.append('【客户合同编号】%s' % o.get('customer_cc'))
    L.append('【账期】%s（授权 %s · %s）' % (o.get('terms') or '', o.get('terms_granted') or '',
                                          o.get('terms_src') or ''))
    L.append('【运输】%s / %s' % (o.get('carrier') or '', o.get('transport') or ''))
    if o.get('ship_note'):
        L.append('【发货要求】%s' % o['ship_note'])
    L.append('【明细】')
    for i, ln in enumerate(o.get('lines') or [], 1):
        L.append('%d. %s %s ｜ %s ｜ 数量 %s %s ｜ 单价 %s ｜ 金额 %s%s'
                 % (i, ln.get('sku') or '', ('（%s）' % ln['spec']) if ln.get('spec') else '',
                    (ln.get('name') or '')[:28], ln.get('qty'), ln.get('unit') or '',
                    ln.get('price'), ln.get('amt'),
                    ('｜ 历史成交价 %s（%s %s）' % (ln.get('base_price'), ln.get('base_date'), ln.get('base_order')))
                    if ln.get('base_price') else '｜ 无历史成交价'))
        if ln.get('note'):
            L.append('   备注：%s' % ln['note'])
        for s in (ln.get('special') or []):
            L.append('   特殊要求：%s' % s)
    L.append('【合计】%s 元' % (o.get('total') or 0))
    if o.get('special_req'):
        L.append('【整体特殊要求】%s' % o['special_req'])
    ap = [x for x in (o.get('approvals') or []) if x.get('level') == 'high']
    if ap:
        L.append('【需审批项】')
        for x in ap:
            L.append('· 【%s】%s' % (x.get('type'), x.get('detail')))
        L.append('【申请原因】%s' % (o.get('reason') or '（未填）'))
        L.append('【审批状态】%s' % (o.get('status') or ''))
    return '\n'.join(L)


# ───────────── 落盘 ─────────────
def _index_path():
    return os.path.join(ORDER_DIR, '_index.json')


def save_order(o):
    os.makedirs(ORDER_DIR, exist_ok=True)
    _dump(os.path.join(ORDER_DIR, '%s.json' % o['id']), o)
    idx = _load(_index_path(), []) or []
    idx = [x for x in idx if x.get('id') != o['id']]
    idx.insert(0, {k: o.get(k) for k in ('id', 'contract', 'customer', 'terms', 'carrier', 'transport',
                                         'total', 'status', 'sales', 'submitted_by', 'created_at',
                                         'line_count', 'approval_count')})
    _dump(_index_path(), idx[:800])


def load_order(oid):
    return _load(os.path.join(ORDER_DIR, '%s.json' % oid))


def visible(user, rec):
    """可见性：admin/gm 全部；审批人 全部；其他人 本人提交 或 本人名下客户"""
    if (user.get('role') or '') in ('admin', 'gm', 'readonly') or is_approver(user):
        return True
    n = user.get('name') or ''
    return n in (rec.get('submitted_by'), rec.get('sales'))


# ───────────── GET ─────────────
def handle_get(path, qs, user):
    if not path.startswith('/api/order/'):
        return None
    a = authz()
    if path == '/api/order/options':
        vis = DEPS['visible_sales'](user) if DEPS['visible_sales'] else None
        ctx = cust_ctx()
        out = []
        for name, v in ctx.items():
            if vis is not None and v.get('sp') not in vis:
                continue
            gd, gl, gsrc = granted_days(name, v.get('sp') or '', a)
            out.append({'name': name, 'sp': v.get('sp') or '', 'dept': v.get('dept') or '',
                        'line': v.get('line') or '', 'grade': v.get('grade') or '',
                        'group': v.get('group') or '',
                        'terms_granted': gl, 'terms_days': gd, 'terms_src': gsrc,
                        'has_terms_record': bool((v.get('terms') or [])),
                        'contracts': (v.get('contracts') or [])[:3],
                        'sr_count': len(v.get('sr_by_sku') or {}),
                        'pay_notes': (v.get('pay_notes') or [])[:2]})
        out.sort(key=lambda x: x['name'])
        me = (a.get('sales') or {}).get(user.get('name') or '') or {}
        return {'ok': True, 'customers': out, 'total': len(out),
                'my_authz': {'terms_label': me.get('terms_label') or (a.get('defaults') or {}).get('terms_label'),
                             'terms_days': me.get('terms_days', (a.get('defaults') or {}).get('terms_days', 7)),
                             'configured': me.get('configured', False),
                             'price_change': me.get('price_change', False),
                             'special_shipping': me.get('special_shipping', False)},
                'is_approver': is_approver(user, a),
                'defaults': a.get('defaults') or {}, 'terms_options': a.get('terms_options') or [],
                'carriers': CARRIERS, 'modes': MODES}

    if path == '/api/order/customer_skus':
        cust = (qs.get('customer') or '').strip()
        h = price_hist().get(cust) or {}
        idx = sku_index()
        out = []
        for sku, v in h.items():
            fams = v.get('fams') or {}
            if not fams:
                continue
            fam, f = max(fams.items(), key=lambda kv: kv[1]['last'].get('d') or '')
            m = idx.get(sku) or {}
            out.append({'sku': sku, 'name': (m.get('name') or '')[:36], 'fam': fam,
                        'price': f['last'].get('p'), 'unit': f['last'].get('u'),
                        'date': f['last'].get('d'), 'order': f['last'].get('no'),
                        'cnt': f.get('cnt'), 'avg12m': f.get('avg12m'),
                        'mc': f['last'].get('mc') or '', 'mcs': v.get('mcs') or [],
                        'bom': bom_brief(sku, f['last'].get('mc') or ''),
                        'opts': m.get('spec_options') or [], 'cls': m.get('spec_class') or '',
                        'pf': bool(m.get('is_pf'))})
        out.sort(key=lambda x: x['date'] or '', reverse=True)
        # 上限 400：现有最大客户 127 个货号（中科绿康），留足余量；total 用于前端提示是否截断
        return {'ok': True, 'skus': out[:400], 'total': len(out), 'truncated': len(out) > 400}

    if path == '/api/order/sku_search':
        q = (qs.get('q') or '').strip().upper()
        if len(q) < 1:
            return {'ok': True, 'skus': []}
        idx = sku_index()
        out = []
        for s, v in idx.items():
            if q in s.upper() or q in (v.get('name') or '').upper():
                out.append({'sku': s, 'name': (v.get('name') or '')[:40], 'unit': v.get('unit') or '',
                            'cls': v.get('spec_class') or '', 'opts': v.get('spec_options') or [],
                            'pf': bool(v.get('is_pf')), 'orders': v.get('orders') or 0,
                            'customers': v.get('customers') or 0})
                if len(out) >= 60:
                    break
        out.sort(key=lambda x: (-x['orders'], x['sku']))
        return {'ok': True, 'skus': out, 'total': len(out)}

    if path == '/api/order/sku_context':
        cust = (qs.get('customer') or '').strip()
        sku = (qs.get('sku') or '').strip()
        spec = (qs.get('spec') or '').strip()
        idx = sku_index().get(sku) or {}
        ctx = cust_ctx().get(cust) or {}
        pi = price_info(cust, sku, spec)
        sr = [x for x in (ctx.get('sr_by_sku') or {}).get(sku, [])]
        gd, gl, gsrc = granted_days(cust, ctx.get('sp') or '', a)
        ship = (_load(SHIP_FILE, {}) or {}).get(cust) or []
        return {'ok': True,
                'sku': {'sku': sku, 'name': idx.get('name') or '', 'unit': idx.get('unit') or '',
                        'cls': idx.get('spec_class') or '', 'opts': idx.get('spec_options') or [],
                        'spec': idx.get('spec') or '', 'pf': bool(idx.get('is_pf')),
                        'bom': idx.get('bom') or '', 'grp': idx.get('grp') or '',
                        'cat': idx.get('cat') or '', 'orders': idx.get('orders') or 0,
                        'customers': idx.get('customers') or 0},
                'price': {'fam': pi.get('fam'), 'pick': pi.get('pick'), 'fams': pi.get('fams'),
                          'units': pi.get('units')},
                'special': sr,
                'bom': bom_info(sku, latest_mc(pi.get('fams'))),
                'customer': {'name': cust, 'sp': ctx.get('sp') or '', 'dept': ctx.get('dept') or '',
                             'grade': ctx.get('grade') or '', 'group': ctx.get('group') or '',
                             'terms_granted': gl, 'terms_granted_label': gl, 'terms_src': gsrc,
                             'terms': ctx.get('terms') or [], 'pay_notes': ctx.get('pay_notes') or [],
                             'sr_common': ctx.get('sr_common') or [], 'contracts': ctx.get('contracts') or [],
                             'shipping': ship[:3]},
                'contract_preview': next_contract_no(qs.get('cc') or '')}

    if path == '/api/order/contract_no':
        return {'ok': True, 'contract': next_contract_no(qs.get('cc') or qs.get('customer_cc') or '')}

    if path == '/api/order/list':
        idx = _load(_index_path(), []) or []
        scope = qs.get('scope') or 'mine'
        out = []
        for x in idx:
            rec = load_order(x.get('id')) or x
            if not visible(user, rec):
                continue
            if scope == 'approve' and not is_approver(user, a):
                continue
            st = qs.get('status')
            if st and st != 'all' and (rec.get('status') or '') != st:
                continue
            out.append({k: rec.get(k) for k in ('id', 'contract', 'customer', 'terms', 'carrier', 'transport',
                                                'total', 'status', 'sales', 'submitted_by', 'created_at',
                                                'approval_count', 'line_count', 'reason')})
            if len(out) >= 200:
                break
        return {'ok': True, 'orders': out, 'total': len(out)}

    if path == '/api/order/detail':
        rec = load_order(qs.get('id') or '')
        if not rec:
            return {'ok': False, 'error': '订单不存在'}
        if not visible(user, rec):
            return {'ok': False, 'error': '无权查看'}
        return {'ok': True, 'order': rec}

    if path == '/api/order/approvals':
        if not is_approver(user, a):
            return {'ok': False, 'error': '仅审批人可查看', 'orders': []}
        idx = _load(_index_path(), []) or []
        out = []
        for x in idx:
            rec = load_order(x.get('id')) or {}
            if (rec.get('status') or '') not in ('待审批',):
                continue
            out.append({k: rec.get(k) for k in ('id', 'contract', 'customer', 'terms', 'terms_granted',
                                                'carrier', 'transport', 'total', 'status', 'sales',
                                                'submitted_by', 'created_at', 'approvals', 'reason',
                                                'lines', 'contact', 'phone', 'address')})
        return {'ok': True, 'orders': out, 'total': len(out)}

    if path == '/api/order/authz':
        if not is_approver(user, a):
            return {'ok': False, 'error': '仅审批人可管理授权'}
        return {'ok': True, 'authz': a, 'terms_options': a.get('terms_options') or [],
                'terms_days_map': TERMS_DAYS}

    if path == '/api/order/my_authz':
        me = (a.get('sales') or {}).get(user.get('name') or '') or {}
        return {'ok': True, 'authz': me, 'defaults': a.get('defaults') or {},
                'customers': (a.get('customers') or {})}

    return {'ok': False, 'error': '未知下单接口: %s' % path}


# ───────────── POST ─────────────
def handle_post(path, body, user):
    if not path.startswith('/api/order/'):
        return None
    a = authz()

    if path == '/api/order/submit':
        cust = (body.get('customer') or '').strip()
        if not cust:
            return {'ok': False, 'error': '请先选择客户'}
        lines_in = body.get('lines') or []
        if not lines_in:
            return {'ok': False, 'error': '请至少添加一个货号'}
        # 权限：该客户是否在登录人可见范围
        vis = DEPS['visible_sales'](user) if DEPS['visible_sales'] else None
        ctx = cust_ctx().get(cust) or {}
        if vis is not None and ctx and ctx.get('sp') not in vis and (user.get('name') not in
                                                                   (ctx.get('sp'), user.get('name'))):
            if (user.get('role') or '') not in ('admin', 'gm'):
                return {'ok': False, 'error': '无权为该客户下单'}
        idx = sku_index()
        lines = []
        for ln in lines_in:
            sku = (ln.get('sku') or '').strip()
            if not sku:
                continue
            spec = (ln.get('spec') or '').strip()
            meta = idx.get(sku) or {}
            pi = price_info(cust, sku, spec)
            base = (pi.get('pick') or {}).get('last') or None
            try:
                qty = float(ln.get('qty') or 0)
            except (TypeError, ValueError):
                qty = 0.0
            try:
                price = float(ln.get('price') or 0)
            except (TypeError, ValueError):
                price = 0.0
            fam = SPEC_FAMILY.get(spec, '')
            base_unit = (base or {}).get('u') or ''
            if base_unit and (('T(' in base_unit and fam == 'T') or (base_unit == 'ml' and fam == 'ml')):
                unit = base_unit                        # 沿用历史成交单位，单价可直接比较
            elif fam == 'T':
                unit = 'T'
            elif fam == 'ml':
                unit = 'ml'
            else:
                unit = (meta.get('unit') or '').split('/')[0]
            fam_warn = ''
            if fam and (pi.get('fams') or {}) and fam not in pi['fams']:
                fam_warn = ('该客户历史成交为 %s 族，与本行所选规格（%s 族）不一致，单价不可直接比较'
                            % ('、'.join(pi['fams'].keys()), fam))
            lines.append({
                'sku': sku, 'name': ln.get('name') or meta.get('name') or '', 'spec': spec,
                'qty': qty, 'unit': unit, 'price': price, 'amt': round(qty * price, 2),
                'pf': bool(meta.get('is_pf')), 'fam': fam, 'fam_warn': fam_warn,
                'base_price': (base or {}).get('p'), 'base_date': (base or {}).get('d'),
                'base_order': (base or {}).get('no'), 'base_unit': (base or {}).get('u'),
                'avg12m': (pi.get('pick') or {}).get('avg12m'),
                'note': (ln.get('note') or '').strip()[:300],
                'special': [str(x)[:300] for x in (ln.get('special') or []) if x],
            })
        if not lines:
            return {'ok': False, 'error': '明细为空'}
        terms = body.get('terms') or ''
        if terms not in TERMS_DAYS:
            return {'ok': False, 'error': '账期取值不合法'}
        o = {
            'id': 'ORD' + datetime.now().strftime('%Y%m%d%H%M%S'),
            'customer': cust, 'contact': (body.get('contact') or '').strip()[:60],
            'phone': (body.get('phone') or '').strip()[:40],
            'address': (body.get('address') or '').strip()[:300],
            'customer_cc': re.sub(r'[^\w\u4e00-\u9fa5\-]', '', (body.get('customer_cc') or '').strip())[:32],
            'terms': terms, 'terms_days': TERMS_DAYS[terms],
            'carrier': body.get('carrier') or '', 'transport': body.get('transport') or '',
            'ship_note': (body.get('ship_note') or '').strip()[:300],
            'special_req': (body.get('special_req') or '').strip()[:600],
            'lines': lines, 'total': round(sum(x['amt'] for x in lines), 2),
            'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'submitted_by': user.get('name') or '', 'sales': ctx.get('sp') or user.get('name') or '',
            'dept': ctx.get('dept') or user.get('dept') or '', 'line': ctx.get('line') or user.get('line') or '',
            'line_count': len(lines), 'status': '', 'reason': (body.get('reason') or '').strip()[:800],
        }
        if o['carrier'] and o['carrier'] not in CARRIERS:
            return {'ok': False, 'error': '承运商不合法'}
        if o['transport'] and o['transport'] not in MODES:
            return {'ok': False, 'error': '运输方式不合法'}
        gd, gl, gsrc = granted_days(cust, o['sales'], a)
        o['terms_granted'], o['terms_granted_days'], o['terms_src'] = gl, gd, gsrc
        aps = eval_approvals(o, user, a)
        o['approvals'] = aps
        if need_reason(aps) and len(o['reason']) < 4:
            return {'ok': False, 'error': '本单触发审批，必须填写原因（≥4字）',
                    'approvals': aps}
        o['approval_count'] = sum(1 for x in aps if x.get('level') == 'high')
        o['status'] = '待审批' if o['approval_count'] else '免审批'
        o['contract'] = next_contract_no(o['customer_cc'], bump=True)
        o['feishu_text'] = feishu_text(o)
        save_order(o)
        # 收件信息记忆
        if o['contact'] or o['address']:
            sh = _load(SHIP_FILE, {}) or {}
            arr = [x for x in (sh.get(cust) or []) if not (x.get('contact') == o['contact'] and x.get('address') == o['address'])]
            arr.insert(0, {'contact': o['contact'], 'phone': o['phone'], 'address': o['address'],
                           'used_at': o['created_at']})
            sh[cust] = arr[:5]
            _dump(SHIP_FILE, sh)
        # 飞书推送审批人
        if o['status'] == '待审批':
            _notify_approvers(o, a)
        return {'ok': True, 'order': o}

    if path == '/api/order/approve':
        if not is_approver(user, a):
            return {'ok': False, 'error': '仅审批人可审批'}
        oid = (body.get('id') or '').strip()
        act = (body.get('action') or '').strip()
        note = (body.get('note') or '').strip()[:600]
        rec = load_order(oid)
        if not rec:
            return {'ok': False, 'error': '订单不存在'}
        if act not in ('approve', 'reject'):
            return {'ok': False, 'error': 'action 必须为 approve/reject'}
        if act == 'reject' and len(note) < 2:
            return {'ok': False, 'error': '驳回必须填写意见'}
        rec.setdefault('history', []).append({'at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                                              'by': user.get('name'), 'action': act, 'note': note,
                                              'from': rec.get('status')})
        rec['status'] = '已通过' if act == 'approve' else '已驳回'
        rec['approved_by'] = user.get('name') or ''
        rec['approved_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        rec['approve_note'] = note
        rec['feishu_text'] = feishu_text(rec)
        save_order(rec)
        return {'ok': True, 'order': rec}

    if path == '/api/order/authz':
        if not is_approver(user, a):
            return {'ok': False, 'error': '仅审批人可管理授权'}
        a2 = save_authz(body or {}, user)
        return {'ok': True, 'authz': a2}

    return {'ok': False, 'error': '未知下单接口: %s' % path}


def _notify_approvers(o, a):
    fn = DEPS.get('notify')
    if not fn:
        return
    txt = ('【下单待审批】%s\n客户：%s\n提交：%s（%s）\n账期：%s（授权 %s）\n运输：%s / %s\n合计：%s 元\n'
           '需审批：%s\n原因：%s' % (
               o.get('id'), o.get('customer'), o.get('submitted_by'), o.get('dept'),
               o.get('terms'), o.get('terms_granted'), o.get('carrier'), o.get('transport'),
               o.get('total'),
               '；'.join(x.get('type') for x in o.get('approvals') or [] if x.get('level') == 'high'),
               o.get('reason') or ''))
    for nm in (a.get('approvers') or []) + [APPROVER_DEFAULT]:
        oid = _open_id_of(nm)
        if oid:
            try:
                fn(oid, txt)
            except Exception:
                pass


def _open_id_of(name):
    for p in (os.path.join(BASE, '报销助手', '用户', '_index.json'),
              os.path.join(BASE, '数据', '_open_ids.json')):
        try:
            d = _load(p)
        except Exception:
            d = None
        if isinstance(d, dict):
            for email, v in d.items():
                if isinstance(v, dict) and v.get('name') == name:
                    return v.get('open_id') or ''
            if d.get(name):
                return d[name]
    return ''
