# Phase 1 Data Model: 订单 ezPay 电子发票开立

**Date**: 2026-06-16

## 新增表：pos_order_invoice（订单电子发票，与 pos_order 1:1）

> DDL 写入 `updatesql/sql.md`，由开发者手动执行（项目规范）。当前态表：一笔订单至多一行；作废后状态置「作废」、允许重开（同行覆盖，MVP 不留历史明细）。

| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT PK AUTO | 主键 |
| order_id | BIGINT NOT NULL | 关联 `pos_order.id`（UNIQUE） |
| order_no | VARCHAR(64) | 冗余 `pos_order.ddId`，便于排查 |
| store_id | BIGINT NOT NULL | 冗余 `pos_order` 所属门店 id（关联 `pos_store.id`） |
| invoice_category | VARCHAR(8) | 发票类型：`B2C` / `B2B` |
| invoice_choice | VARCHAR(16) | 发票选择(下单意图) PAPER/PHONE_BARCODE/CITIZEN/LOVE_CODE/COMPANY; NULL=不开票 |
| buyer_name | VARCHAR(100) | 买方名称（B2C=客户名/邮箱名，B2B=公司名） |
| buyer_ubn | VARCHAR(16) | 买方统一编号（统编 8 码，B2B 必填） |
| buyer_email | VARCHAR(200) | 接收邮箱（B2C 无载具时用） |
| carrier_type | VARCHAR(8) | 载具类型：`0`手机条码/`1`自然人凭证/`2`ezPay会员载具（可空） |
| carrier_num | VARCHAR(64) | 载具号码（carrier_type 非空时必填） |
| invoice_number | VARCHAR(16) | ezPay 发票号（开立成功后填，字轨号段分配） |
| random_num | VARCHAR(8) | 防伪随机码（ezPay 返回） |
| invoice_status | TINYINT NOT NULL DEFAULT 0 | `0`未开 / `1`已开 / `2`失败 / `3`作废 |
| issue_triggered | TINYINT NOT NULL DEFAULT 0 | 是否已触发开票 0否1是;出餐开票执行过=1,与 invoice_status 独立(下单捕获意图=0) |
| total_amt | INT | 含税总额（= 订单实付 amount） |
| sales_amt | INT | 销售额（未税，= round(total/1.05)） |
| tax_amt | INT | 税额（= total - sales） |
| fail_reason | VARCHAR(500) | 开票/作废失败原因 |
| invoice_trans_no | VARCHAR(64) | ezPay 交易流水号 InvoiceTransNo（开立回应返回） |
| invoice_bar_code | VARCHAR(255) | 发票条码码值（PrintFlag=Y 时 ezPay 返回，兑奖扫描用） |
| invoice_qrcode_l | VARCHAR(255) | 发票左二维码码值（PrintFlag=Y 时返回，含发票核心信息+加密防伪） |
| invoice_qrcode_r | VARCHAR(255) | 发票右二维码码值（PrintFlag=Y 时返回，含中文商品明细） |
| apply_time | DATETIME | 客户申请开票时间 |
| issue_time | DATETIME | 开立成功时间 |
| invalid_time | DATETIME | 作废时间 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
| create_by | VARCHAR(64) | 创建人 |
| update_by | VARCHAR(64) | 更新人 |

**索引**：
- `UNIQUE KEY uk_order_id (order_id)` —— 防重复开票（D5）
- `KEY idx_status (invoice_status)`
- `KEY idx_store (store_id)`

## 状态机（invoice_status）

```
        开票成功              作废(ezPay invalid 成功)
  ┌─────────────────┐   ┌──────────────────┐
  ▼                 │   ▼                  │
[0 未开] ──成功──> [1 已开] ──作废──> [3 作废]
  │                 │
  │ 失败            │ 失败
  ▼                 ▼
[2 失败] <──重试─── (失败/作废/未开 均可重试开票)
  └──重试成功──> [1 已开]
```

- 开票前置：`invoice_status ∈ {0 未开, 2 失败, 3 作废}` 才允许发起开票；`1 已开` 拒绝（FR-005）。
- 重开：作废(3) 后可再次开票 → 状态先置 0、再走开票成功置 1（同行，新 invoice_number 覆盖旧号）。

> **issue_triggered (方案B, 与 invoice_status 独立)**: 下单捕获意图时插 `pos_order_invoice` 行, `issue_triggered=0`、`invoice_status=0`; 出餐开票流程执行过即置 `issue_triggered=1` (无论成功/失败/作废)。管理端「订单发票管理」按 `issue_triggered=1` 筛选, 排除仅下单捕获、未触发的意图行。

## 复用实体（不改）

- **PosOrder**（`pos_order`）：开票来源。取 `id`/`ddId`/`amount`/`food`/`state`(=3 完成)/`payStatus`(=1)/门店归属（`shId`/`mdId`）。门店 id 判定：依据 CLAUDE.md 商家类型口径（普通/夜市商家用 `shId`，摊位用 `mdId`）对齐 `pos_store`。
- **PosStoreEzpay**（009，`pos_store_ezpay`）：开票凭证来源。仅 `ezpayStatus=2 且 isEnabled=1` 的门店可开票；取 `merchantId`/`hashKey`/`hashIv` 构造 `EzPayConfig`。
- **PosStore**（`pos_store`）：`invoice_exempt=1` 的门店不提供开票（009）。

## 金额拆分（开票时计算，不入订单表）

按 research D1（**不含运费**，订单构成 `amount = foodAmount − 优惠 + freight`）：
```
invoice_total = amount - freight            // 商品实付(已扣优惠、不含运费)
total_amt     = invoice_total               // 含税总额
sales_amt     = round(invoice_total / 1.05) // 销售额未税
tax_amt       = total_amt - sales_amt       // 税额
```
逐商品明细由 `food` JSON 组装（D2），优惠按比例分摊到各商品使 ΣItemAmt = total_amt（D3）；运费不进发票。

## 新增表：pos_love_org（捐赠机构字典，US6）

> DDL + 数据在 `updatesql/pos_love_org.sql`（由 `specs/010-order-invoice/gen_love_org_sql.py` 从财政部 CSV 生成），开发者手动执行。全平台共享的社福机构字典（非门店级）。

| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT PK AUTO | 主键 |
| love_code | VARCHAR(10) NOT NULL UNIQUE | 捐赠码（爱心码，**含前导零须字符串存**，3-7 位） |
| org_name | VARCHAR(255) NOT NULL | 受捐赠机关或团体名称 |
| org_short_name | VARCHAR(64) | 机构简称（可空） |
| ubn | VARCHAR(16) | 统编（含前导零，可空） |
| city | VARCHAR(32) | 县市（可空） |
| enabled | TINYINT NOT NULL DEFAULT 1 | 1启用 / 0停用（>7 位码 ezPay 不支持，置 0） |
| sync_time | DATETIME | 财政部清单同步时间 |
| create_time | DATETIME | 创建时间 |

**索引**：`UNIQUE KEY uk_love_code (love_code)`。

## pos_order_invoice 扩展（US6）

| 新增字段 | 类型 | 说明 |
|----------|------|------|
| love_code | VARCHAR(10) | 捐赠码（开捐赠发票时填，否则空） |
| love_org_name | VARCHAR(255) | 捐赠机构名（开票时从 pos_love_org 快照，便于历史展示） |

> ALTER 写入 `updatesql/sql.md`。捐赠发票 invoice_category 仍为 `B2C`（ezPay 捐赠即 B2C+LoveCode），靠 `love_code` 非空区分捐赠与普通 B2C。
