# FRIDAY SaaS Cold Import Manifest(首版 — 仅 patient 试跑)
#
# 数据源:FRIDAY SaaS MySQL(customer 库)。cold-import 不支持 MySQL 直连,
#   走 CSV 文件模式。宿主推「自洽业务表」:自己的引用(私有字典码 / 1:N 默认号 / 头-行属性)
#   在导出时 within-host join 后 inline 进相关行,**列名沿用宿主原表列名**(见 tables 上方说明);
#   业务 WHERE / status 切分等全部留在 PAC transforms(单一真理源)。
#
# ── 方案 B:合成"市场"集团 tenant;品牌降为 source_unit 命名空间 ──
#   FRIDAY 是多品牌 SaaS(30+ 独立品牌 tenant_id,各含 1-8 诊所 organization_id)。
#   PAC tenant = 合成 friday-market(整个市场一个租户,便于 FRIDAY 作为运营方跨品牌召回);
#   品牌(源 tenant_id 列,宿主原生名)→ patients.source_unit(集团内给 external_id 消歧:同号不同品牌=不同人);
#   诊所 organization_id → 后续 orgScope 隔离维度。
#   ⚠️ 隔离靠 orgScope/source_unit,非 tenant 边界 → 生产必须「禁止空 orgScope」。
#      若要品牌级硬隔离,改为 tenant_field: tenant_id + tenant_map(每品牌一个 PAC tenant)。
#
# 跑法:
#   pnpm sync -- --dir=./data/friday --dry-run     # 看翻译效果,不写库
#   pnpm sync -- --dir=./data/friday --no-recompute

host_name:    friday
display_name: FRIDAY SaaS(多品牌市场)
auto_sync:    false

tenant_id:                friday-market   # PAC 租户 = 合成市场根(静态)
identity_namespace_field: tenant_id       # patients.source_unit ← 源品牌列(宿主原生列名;与 PAC 租户概念的区分在 PAC 侧,宿主不改名)

amount_unit: yuan
timezone:    Asia/Shanghai

# 诊所名字典来源:病历表自带 organization_name(宿主原生列)。
# `pnpm refresh-clinic-names -- --dir=./data/friday` 据此 DISTINCT 出 id→名,写 host.clinicNames;
# /auth/session 合并进 dictionary.clinics 下发 → 工作台显示诊所中文名而非 GUID。
clinic_directory:
  table:      med_emr_info
  id_field:   organization_id
  name_field: organization_name


# ── 增量水位声明(2026-07 与 jvs-dw 统一;file 源放 manifest 顶层)──
# file 装载不消费水位(每轮全量装载,幂等去重兜底);每轮跑完与 jvs-dw 走同一段 finally
# 记账 cursor_after = run_start(sync_logs)。用途:delta 导出 WHERE 模板 / push 回放起点。
# 只列有 updated_gmt_at 的事件表(字典/联系方式/计划头等已 inline,不再单独摄入)。
incremental:
  per_query:
    customer_basic_info:            { cursor_column: updated_gmt_at }
    appointment_base:               { cursor_column: updated_gmt_at }
    med_emr_info:                   { cursor_column: updated_gmt_at }
    med_check:                      { cursor_column: updated_gmt_at }
    patient_settlement:             { cursor_column: updated_gmt_at }
    patient_settlement_spec:        { cursor_column: updated_gmt_at }
    customer_treat_plan_item:       { cursor_column: updated_gmt_at }
    customer_referee_circle:        { cursor_column: updated_gmt_at }
    customer_consult:               { cursor_column: updated_gmt_at }

# ⭐ 宿主推「自洽的业务表」:私有字典码 / 1:N 派生 / 头-行属性 都由宿主 within-host join 后 inline 进相关行
#   (契约见 docs/integration/friday-push-payload)。故不再摄入纯字典表(std_diag/std_check_class)、
#   联系方式 1:N(customer_contacts→inline 默认号进主档)、计划头(customer_treat_plan→inline 进行)、
#   支付通道(settlement_modes,金额已在结算头 net_receipts_this)。所有摄入路径(push/file/pull/reparse)同此形态。
tables:
  - { table: customer_basic_info, file: customer_basic_info.csv }   # 患者主档(含 inline contacts_tel/relationship)
  - { table: appointment_base,    file: appointment_base.csv }      # 预约(原始,全状态)
  # ── EMR 病历链(W FRIDAY-EMR:Mongo med_emr_info status∈{3,4} 正式病历;export.sh 平铺导出)──
  # diag[] 元素含 inline std_code(宿主解析 linkCode→std_diag.std_code);treat/dispose/examine 原生数组。
  - { table: med_emr_info,            file: med_emr_info.json }     # 病历正文(Mongo,含 diag/treat 数组)
  - { table: med_check,               file: med_check.csv }         # 影像档案(含 inline class_name;挂 emr_id)
  - { table: customer_treat_plan_item, file: customer_treat_plan_item.csv } # 治疗计划行(含 inline organization_id/plan_name)
  # 结算原表(全 status,导出侧不做业务 WHERE)—— 消费/退费/明细全部由 PAC transforms 切分(单一真理源)
  - { table: patient_settlement,      file: patient_settlement.csv }       # 结算头单原表(全 status)
  - { table: patient_settlement_spec, file: patient_settlement_spec.csv }  # 结算明细原表(全量,含 is_refund)
  - { table: customer_referee_circle, file: customer_referee_circle.csv }    # 转介绍圈(患者-患者边)
  - { table: customer_consult,        file: customer_consult.csv }           # 咨询(意向/未成交原因)

transforms:
  # ── 患者 phone:宿主已按「默认号→本人→最早」挑好、inline 进 customer_basic_info 的
  #    contacts_tel / relationship 两列(沿用宿主源列名;不再 lookup customer_contacts)。patient.yaml 直接读。──

  # ── 预约:丢草稿 → 全部预约(拼 scheduledAt)+ 已到诊拆成接诊事件(→ encounter)──
  # 与 jvs-dw 同款设计:一个预约源喂两张 canonical(appointment / encounter)。
  # status 编码两家一致(1正常/2改约/3取消/4到诊/5接诊/6结算/8walkin/9爽约);10=草稿丢弃。
  - kind: filter
    input: appointment_base
    output: _appt_valid
    where:
      appointment_status: { in: ['1', '2', '3', '4', '5', '6', '8', '9'] }   # 丢草稿 10
  # 全部预约:拼精确预约时刻(appointment_datetime_str 是区间且 14% 空 → 用 date + start)
  - kind: derive
    input: _appt_valid
    output: appointment_all_rows
    fields:
      scheduled_at_ts:
        op: concat
        parts: ['${appointment_date}', ' ', '${appointment_start}']   # "2026-07-13 15:30:00"
  # 已到诊(4/5/6)且有到诊时刻 → 接诊事件(startedAt = in_time)
  - kind: filter
    input: _appt_valid
    output: appointment_encounter_rows
    where:
      appointment_status: { in: ['4', '5', '6'] }
      in_time: { not_empty: true }

  # ═══════════ EMR 病历链(同 jvs-dw 分化模式:一张病历源喂多 canonical)═══════════
  # med_emr_info → emr_record(自由文本) + diagnosis(diag[] 拆行) + treatment_actual(treat[] 拆行)
  # 锚点:临床事实 sourceEncounterExternalId=emr_sub_id(同诊聚合);emr 自身挂 appointment_id(56.5%)。

  # ── E.1 病历自由文本行(emr.yaml 直接消费 med_emr_info,无需 project:
  #        assembler field_mapping 天然只取所需列,数组列不映射即不入 canonical)──

  # ── E.0 病历排草稿:status∈{3,4} 正式病历(迁自 mongo 导出侧过滤;PAC 侧统一)──
  - kind: filter
    input: med_emr_info
    output: _emr_official
    where:
      status: { in: ['3', '4'] }

  # ── E.2 diag[] 拆行 → 诊断候选 ──
  - kind: split_json_array
    input: _emr_official
    output: _diag_raw
    array_field: diag
    parent_keys:
      emr_sub_id: emr_sub_id
      emr_id: emr_id
      tenant_id: tenant_id
      organization_id: organization_id
      patient_id: patient_id
      user_id: user_id
      user_name: user_name
      clinic_time: clinic_time
      created_gmt_at: created_gmt_at
      updated_gmt_at: updated_gmt_at
    element_fields:
      value: value
      tooth_position: toothPosition
      std_code: std_code         # 宿主已解析 linkCode→std_diag.std_code、inline 进元素(沿用宿主列名;不再 PAC 侧 lookup)
    where:
      value: { not_empty: true }    # 只要求有诊断名;std_code 可空(非 K/未解析 → 走自由文本)

  # E.2.2 归一 + K 码截取 + coalesce
  # 规则(用户定):std_code K 开头 → 截 K 大类;非 K(DA0E.50 等 ICD-11)/join 不上/linkCode 空
  #   → 自由文本处理:substring 产物("DA0"等)不在共享 K 码字典 → 落 _default → code=null,name 保留。
  #   linkCode 空(99.7%)→ coalesce 落到归一中文名 → 共享 diagnosis-kcode 字典救码。
  - kind: derive
    input: _diag_raw
    output: _diag_norm
    fields:
      value_norm:
        op: normalize
        from: value
      std_code_k:
        op: substring
        from: std_code
        start: 0
        end: 3
  - kind: derive
    input: _diag_norm
    output: diagnosis_rows
    fields:
      diag_code_src:
        op: coalesce
        from: ['std_code_k', 'value_norm']
      diag_external_id:
        op: concat
        parts: ['${emr_sub_id}', '|', '${std_code_k}', '|', '${value_norm}', '|', '${tooth_position}']

  # ── E.3 treat[] 拆行 → 本次治疗(actual;用户确认 treat=本次治疗,病历无计划字段)──
  # 注:treat 填充仅 25%(dispose 98% 是处置叙述,整段进 emr_record.disposal 不丢);
  #    若后续漏召,可加 dispose keyword 路由补 actual(Phase 2 评估)。
  - kind: split_json_array
    input: _emr_official
    output: _treat_raw
    array_field: treat
    parent_keys:
      emr_sub_id: emr_sub_id
      emr_id: emr_id
      tenant_id: tenant_id
      organization_id: organization_id
      patient_id: patient_id
      user_id: user_id
      user_name: user_name
      clinic_time: clinic_time
      created_gmt_at: created_gmt_at
      updated_gmt_at: updated_gmt_at
    element_fields:
      value: value
      tooth_position: toothPosition
    where:
      value: { not_empty: true }
  # E.3.1 归一(新列 value_norm;external_id 仍用原始 value,保持与已摄入数据的幂等键连续)
  - kind: derive
    input: _treat_raw
    output: _treat_raw
    fields:
      value_norm:
        op: normalize
        from: value

  # E.3.2 三路分流(同 jvs-dw C.3 语义):建议→recommendation / 流程复查→review / 其余→actual
  # 实测(2026-07-18 全量):建议/推荐开头 153 行、复查类 60 行(其中拆线 50——不入 review,
  # 按 jvs-dw 2026-06-11 一线判例 拆线=术后护理=外科治疗动作,走 default→actual→enum 拆线→surgical)
  - kind: route_by_pattern
    input: _treat_raw
    field: value_norm
    routes:
      - output: _rec_raw
        when:
          starts_with: ['建议', '推荐']
      # 流程/复查/行政词表(取自 jvs-dw review_terms 核心,normalize 后 ASCII 形态)
      - output: _review_raw
        when:
          equals:
            - 常规复查
            - 复查
            - 定期复查
            - 检查
            - 初诊检查
            - 正畸复查
            - 正畸检查
            - 正畸复诊
            - 种植复查
            - 牙周复查
            - 保持器复诊
            - 暂观
            - 观察
            - 观察随访
            - 随诊观察
            - 随访观察
            - 无治疗
            - 重复
            - 取资料
            - 转诊
            - 正畸咨询
            - 正畸会诊
            - 方案沟通
            - 沟通治疗方案
            - 听方案
            - 缴费
            - 未拍片
      - output: _actual_raw
        when:
          default: true

  # E.3.3 真治疗 → actual
  - kind: derive
    input: _actual_raw
    output: treatment_actual_rows
    fields:
      category_raw:
        op: coalesce
        from: ['value_norm']
      treat_external_id:
        op: concat
        parts: ['${emr_sub_id}', '|treat|', '${value}', '|', '${tooth_position}']

  # E.3.4 流程复查 → review(category 固定哨兵,assembler 翻成 review)
  - kind: derive
    input: _review_raw
    output: treatment_review_rows
    fields:
      category_raw:
        op: concat
        parts: ['_review_sentinel']
      treat_external_id:
        op: concat
        parts: ['${emr_sub_id}', '|treat|', '${value}', '|', '${tooth_position}']

  # E.3.5 建议/推荐 → recommendation(共享 recommendation-code 字典翻召回码)
  - kind: derive
    input: _rec_raw
    output: recommendation_rows
    fields:
      rec_external_id:
        op: concat
        parts: ['${emr_sub_id}', '|rec|', '${value}', '|', '${tooth_position}']

  # ── E.4 影像:med_check 已含 inline class_name(宿主解析 class_code→std_check_class);
  #    image.yaml 直接以 med_check 为 primary、读 class_name 翻 modality(不再 PAC 侧 lookup)。──

  # ── E.5 治疗计划:计划行已含 inline organization_id/plan_name(宿主取自计划头);
  #    仅丢弃无诊所归属的行(canonical clinicId 必填;头无诊所 → 该行产 failed 噪音,提前剔除)。──
  - kind: filter
    input: customer_treat_plan_item
    output: _plan_item_with_org
    where:
      organization_id: { not_empty: true }
  - kind: derive
    input: _plan_item_with_org
    output: treatment_planned_rows
    fields:
      mode_norm:
        op: normalize
        from: mode_name

  # ═══════════ 结算链(消费/退费;对齐 jvs-dw payment/refund 契约)═══════════
  # status 语义(实测):1=已结算 3=含退费行的单(头单仍是原始消费) 4=整单反向冲减。
  # 消费 = status∈{1,3};退费双轨 = ①status=4 整单(ref_settlement_id 挂原单)②spec is_refund=1 行级。
  # LTV = Σpayment − Σrefund 自然对账;不从 settlement 反推治疗(财务≠临床,jvs-dw 已验证的教训)。

  # ── S.1 消费:status∈{1,3} 且应收≥0 → payment(金额取结算头 net_receipts_this)。
  #    支付通道(settlement_modes)暂不摄入 → payment 无 method(总值在结算头,无需通道拆分)。──
  - kind: filter
    input: patient_settlement
    output: payment_rows
    where:
      status: { in: ['1', '3'] }
      receivable_this: { gte: 0 }      # 应收≥0(迁自导出侧 WHERE;数值算子)
      net_receipts_this: { gte: 0 }    # ⭐ 实收≥0 —— 见 S.2 第③种表达,负实收归退费不归消费

  # ── S.2 退费①:整单冲减 —— PAC 侧切分(不再靠导出 WHERE)。**三种**表达 union:
  #    ① status=4 整单反向;
  #    ② status=3 且 receivable_this<0(2ac96f6d 品牌用负应收表达退费);
  #    ③ status∈{1,3} 且 receivable_this≥0 但 net_receipts_this<0 —— 应收不动、只把钱退回去。
  #    金额负值由 refund parser Math.abs 归一为正 cents。
  #
  #    ⚠️ ③ 是 2026-07-29 线上事故补的,别当冗余删掉:
  #      漏了它 → 这些行落进 payment_rows → payment_record 的 amount_cents 为负 → zod 拒收
  #      → bulkWrite 整批失败 → 降级逐行事务 → 单批 200 行从 3 秒涨到 63.5 秒
  #      → 超过宿主 60s 读超时 → 对方永远收不到成功回执、无限重推同一批(实测卡了 13 小时)。
  #      测试服实测:57,939 条结算里这样的只有 **4 行**(status=3 / 应收 0 或 6000 / 实收 -500~-6000),
  #      与 ①② 零重叠(全库无 status=4、无应收<0)。
  - kind: filter
    input: patient_settlement
    output: _refund_status4
    where:
      status: { equals: '4' }
  - kind: filter
    input: patient_settlement
    output: _refund_neg3
    where:
      status: { equals: '3' }
      receivable_this: { lt: 0 }       # 数值算子:负额退费表达
  - kind: filter
    input: patient_settlement
    output: _refund_neg_receipt
    where:
      status: { in: ['1', '3'] }
      receivable_this: { gte: 0 }      # 与 ② 互斥(② 要求应收<0),union 不会重复计
      net_receipts_this: { lt: 0 }
  - kind: union
    inputs: ['_refund_status4', '_refund_neg3', '_refund_neg_receipt']
    output: refund_full_rows

  # ── S.3 退费明细②:行级退费(patient_settlement_spec is_refund=1)——PAC 侧 filter ──
  #    ⚠️ spec.patient_id 部分品牌是「诊所本地 id」而非全局 customer id(元和王永 691828a5 实测
  #    全 26421 行不符,spec=2/头=666716)。退费明细是结算头子行 → 宿主导出时已按 settlement_id
  #    从结算头把真 patient_id **反填进 spec.patient_id**(within-host join,inline),PAC 直接信;
  #    故此处仅 filter,不再跨表 lookup 结算头(单表 push 自洽)。
  - kind: filter
    input: patient_settlement_spec
    output: refund_item_rows
    where:
      is_refund: { equals: '1' }

  # ═══════════ 患者关系链(customer_referee_circle → patient_relations 独立表)═══════════
  # 宿主 referee_relationship 是字典 id(DB 无字典表,疑似代码硬编码)。解码 = 统计推断
  # (2026-07-19,互逆对+年龄差+性别三重证据,详见 patient_relation.yaml)——需要对方性别
  # 参与消歧(码 3=父母,father/mother 按对方性别拆)→ lookup 对方 sex 后拼 "码|性别" 键。
  - kind: lookup
    input: customer_referee_circle
    output: _referee_with_sex
    from: customer_basic_info
    left_key: referee_patient_id
    right_key: id
    select:
      referee_sex: sex
    # push 是单表请求,customer_basic_info 不在场 → 从 PAC 已入库的 patients 重建对方性别。
    # 不配这段的话:所有行 referee_sex=null → 码 3 拼出 "3|" → 掉 _default: other,
    # father/mother 永远为 0(2026-07-28 实测:friday 257 条边 father+mother=0,
    # 同口径走 cold-import 的 jvs-dw 这两类占 23%),连带「多代之家」画像系统性漏判。
    # map 把 PAC 归一值反写回宿主原编码,保证 push 与 cold-import 产出同一个 rel_sex_key。
    push_fallback:
      entity: patients
      select:
        sex: { column: gender, map: { male: '1', female: '2' } }
  - kind: derive
    input: _referee_with_sex
    output: patient_relation_rows
    fields:
      rel_sex_key:
        op: concat
        parts: ['${referee_relationship}', '|', '${referee_sex}']

assemblers:
  - { file: assemblers/patient.yaml }
  - { file: assemblers/appointment.yaml }
  - { file: assemblers/encounter.yaml }
  - { file: assemblers/emr.yaml }
  - { file: assemblers/diagnosis.yaml }
  - { file: assemblers/treatment_actual.yaml }
  - { file: assemblers/treatment_planned.yaml }
  - { file: assemblers/image.yaml }
  - { file: assemblers/treatment_review.yaml }
  - { file: assemblers/recommendation.yaml }
  - { file: assemblers/payment.yaml }
  - { file: assemblers/refund_full.yaml }
  - { file: assemblers/refund_item.yaml }
  - { file: assemblers/patient_relation.yaml }
  - { file: assemblers/consult.yaml }
