# Feature Specification: 商家 ezPay 发票开通管理

**Feature Branch**: 不新建分支（在当前 test 分支开发）

**Created**: 2026-06-15

**Status**: Draft

**Input**: User description: "商家 ezPay 发票开通管理：平台代商家到 ezPay（线下人工）注册申请发票功能，申请通过拿到 HashKey/HashIV 后由运营录入并验证，验证通过标记已开通才能开发票。平台后台管理开通状态、哪些门店还没开通、哪些还要去开通；部分商家/门店免用发票不进流程。已有 EzPay 开票工具类，本功能只做管理+凭证录入，不含订单自动开票。"

## User Scenarios & Testing *(mandatory)*

### User Story 1 - 运营查看门店 ezPay 开通全景并定位待办 (Priority: P1)

平台运营在后台打开"ezPay 发票开通管理"页面，看到所有需要开票的门店（普通商家店铺 + 夜市下摊位门店）及其 ezPay 开通状态。运营能按状态筛选，并用"还没开通""还要去开通"快捷过滤，立刻知道哪些门店需要推动申请、哪些卡在审核、哪些已就绪。免用发票的门店清晰标注、不混进待办。

**Why this priority**: 这是本功能的核心价值——让运营对全平台发票开通进度一目了然并采取行动。没有它，运营无法知道谁还没开通、谁要去开通，整个线下申请流程无法推进。

**Independent Test**: 可通过"打开列表 → 用'还要去开通'过滤 → 得到一批待申请门店"独立验证，交付"运营知道下一步找谁"的价值。

**Acceptance Scenarios**:

1. **Given** 平台有若干门店（含普通店铺、夜市下摊位、免用发票门店），**When** 运营打开 ezPay 开通管理列表，**Then** 列表展示每个门店的名称、ezPay 状态（未申请/申请中/已开通/免用发票）、启用开关、统编是否已填。
2. **Given** 列表已加载，**When** 运营点击"还要去开通"过滤，**Then** 仅显示"需开票且未申请"的门店，免用发票与已申请门店均不在结果中。
3. **Given** 列表已加载，**When** 运营按状态筛选"申请中"，**Then** 仅显示状态为申请中的需开票门店。

---

### User Story 2 - 运营发起申请并录入凭证验证开通 (Priority: P1)

运营为某门店向 ezPay 线下提交注册申请后，在后台把该门店状态推进为"申请中"；ezPay 审核通过、运营拿到专属凭证（商店代号 / HashKey / HashIV）后，回到后台录入凭证并触发验证。系统调用 ezPay 接口验证凭证可用，验证通过才标记为"已开通"并自动启用。

**Why this priority**: 这是把"申请"变成"能开发票"的唯一路径，直接决定商家能否开票，与 US1 同为核心。

**Independent Test**: 可通过"对某测试门店录入 ezPay 测试环境的真凭证 → 验证通过 → 状态变已开通"独立验证，交付"该门店具备开票前提"的价值。

**Acceptance Scenarios**:

1. **Given** 某门店处于"未申请"，**When** 运营点击"发起申请"，**Then** 状态变为"申请中"，记录申请时间。
2. **Given** 某门店处于"申请中"，**When** 运营录入正确的 ezPay 凭证并点"验证并开通"，**Then** 系统调用 ezPay 接口验证通过，状态变为"已开通"、启用开关打开、记录开通时间。
3. **Given** 某门店处于"申请中"，**When** 运营录入错误的 HashKey/HashIV 并点"验证并开通"，**Then** 系统识别金钥错误，拒绝开通，状态保持"申请中"，并回传可理解的错误提示。
4. **Given** 验证调用发生网络异常，**When** 运营点"验证并开通"，**Then** 不开通、不改变状态，提示运营稍后重试。

---

### User Story 3 - 运营标记免用发票门店 (Priority: P2)

部分商家/门店是小规模商家或摊位，适用免用发票（不开统一发票、只开收据）。运营在后台把这类门店标记为"免用发票"，它们从此退出 ezPay 开通待办，不被当作"还没开通/还要去开通"。

**Why this priority**: 不标记会造成待办列表被本就不需要开票的门店淹没，但它是优化体验而非核心开通链路，故 P2。

**Independent Test**: 可通过"把一个门店标记免用发票 → 它从'还要去开通'过滤中消失"独立验证。

**Acceptance Scenarios**:

1. **Given** 某门店为需开票状态，**When** 运营点击"标记为免用发票"，**Then** 该门店不再出现在"还没开通""还要去开通"过滤结果中，列表中以"免用发票"标签展示。
2. **Given** 某门店被标记为免用发票，**When** 运营点击"恢复需开票"，**Then** 该门店回到需开票门店集合，按其 ezPay 状态参与筛选。

---

### User Story 4 - 商家在商家端上传门店统一编号 (Priority: P2)

运营代商家向 ezPay 申请发票时需要商家的统一编号（统编）。商家在商家端门店设置里填写并保存统编，供运营申请时使用。

**Why this priority**: 统编是申请的前置材料，但没有它运营仍可先发起申请流程；属于支持性输入，故 P2。

**Independent Test**: 可通过"商家在门店设置填入统编 → 保存 → 运营后台该门店显示统编已填"独立验证。

**Acceptance Scenarios**:

1. **Given** 商家打开门店设置，**When** 商家填写统一编号并保存，**Then** 统编保存成功，平台后台该门店显示统编已填写。
2. **Given** 某门店已被标记为免用发票，**When** 商家查看门店设置，**Then** 统编字段非必填/不强制（免用门店无需统编）。

---

### User Story 5 - 运营停用/恢复已开通门店 (Priority: P3)

门店已开通 ezPay 发票后，运营可临时停用（如商家暂停营业、凭证需更换），停用期间视为不可开票；之后可一键恢复，无需重新走申请与验证。

**Why this priority**: 已开通门店的运行时开关，偶发但必要；不影响开通链路本身，故 P3。

**Independent Test**: 可通过"对已开通门店点停用 → 启用开关关闭 → 再点恢复 → 开关打开且状态仍为已开通"独立验证。

**Acceptance Scenarios**:

1. **Given** 某门店为"已开通且启用"，**When** 运营点击"停用"，**Then** 启用开关关闭，状态保持"已开通"。
2. **Given** 某门店为"已开通但停用"，**When** 运营点击"恢复"，**Then** 启用开关打开，无需重新验证凭证。

---

### Edge Cases

- 门店从"已开通"改为"免用发票"（罕见）：保留 ezPay 凭证数据，以免用发票标识为准，不自动删除凭证。
- 运营对未上传统编的门店发起申请：系统拒绝推进为"申请中"（统编为发起申请的硬性前置，不可后补），提示运营先让商家在门店设置补全统编。
- 同一组 ezPay 凭证被录入到多个门店：本期不强制唯一，仅记录与提示。
- 凭证录入后 ezPay 测试环境不可用：验证失败按"网络异常"分支处理，允许稍后重试，不锁死状态。
- 夜市（userType=3）本身不开票：其账号不进入门店 ezPay 列表；夜市下的摊位门店（pos_store）正常参与。

## Requirements *(mandatory)*

### Functional Requirements

- **FR-001**: 系统必须能按门店（pos_store）记录 ezPay 申请状态（未申请 / 申请中 / 已开通）与启用开关（启用 / 停用）。
- **FR-002**: 系统必须为门店新增"是否免用发票"属性；该属性决定门店是否纳入 ezPay 开通流程。
- **FR-003**: 平台后台必须提供门店 ezPay 开通管理列表，展示门店名称、ezPay 状态、启用开关、统编填写情况，支持按状态筛选（全部 / 免用发票 / 未申请 / 申请中 / 已开通）与门店名搜索。
- **FR-004**: 平台后台必须提供"还没开通"（需开票且未申请或申请中）和"还要去开通"（需开票且未申请）两个快捷过滤，且二者均排除免用发票门店。
- **FR-005**: 运营必须能将门店在"未申请 → 申请中 → 已开通"之间推进，并记录申请时间与开通时间。
- **FR-006**: 运营必须能为门店录入 ezPay 凭证（商店代号 / HashKey / HashIV）；录入时系统必须调用 ezPay 接口验证凭证可用性。
- **FR-007**: 仅当凭证验证通过时系统才标记为"已开通"并启用；凭证金钥错误时必须拒绝开通、保留原状态并返回错误说明。
- **FR-008**: 运营必须能对"已开通"门店切换启用开关（停用 / 恢复），切换不影响申请状态、不要求重新验证。
- **FR-009**: 运营必须能将门店标记为"免用发票"或恢复为"需开票"；标记为免用发票后该门店退出 ezPay 待办过滤。
- **FR-010**: 商家必须在商家端能为门店上传统一编号（统编）；免用发票门店的统编不强制。
- **FR-011**: 只有"已开通且启用"的门店才被视为具备开票前提；本功能仅维护该状态，不含订单完成时的自动开票逻辑。
- **FR-012**: 平台前端与商家前端所有新增面向用户文字必须实现四语言（vi / zh / tw / en）国际化。
- **FR-013**: 运营发起申请（未申请→申请中）前，统编为硬性前置：门店未上传统编或统编非 8 位数字时，系统必须拒绝推进、保留"未申请"状态，并返回"请先让商家在门店设置填写统编"提示；平台前端对未上传统编的门店禁用"发起申请"按钮并以 tooltip 说明。

### Key Entities *(include if feature involves data)*

- **PosStore（门店，已有）**：代表一个营业档口/店铺，是 ezPay 凭证的绑定单位。新增"是否免用发票"属性。一个门店对应一组 ezPay 凭证。
- **门店 ezPay 配置（新增，与门店 1:1）**：记录该门店的 ezPay 申请状态、启用开关、商店代号（MerchantID）、HashKey、HashIV、会员编号（CompanyID，可选）、统一编号（统编）、申请时间、开通时间、最近一次凭证验证结果、备注。门店没有此配置行即视为"未申请"。
- **ezPay 加值服务平台（外部）**：商家发票加值中心；本功能仅用其只读验证接口校验凭证，注册申请为线下人工。

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: 运营能在单一列表查看全平台需开票门店的 ezPay 开通状态，并能在 3 次操作内定位到"还要去开通"的门店清单。
- **SC-002**: "还没开通""还要去开通"过滤结果中，免用发票门店的出现率为 0。
- **SC-003**: 运营录入错误 ezPay 凭证时，系统 100% 拒绝开通并给出可理解的错误提示；任何错误凭证都不会进入"已开通"状态。
- **SC-004**: 运营录入正确 ezPay 凭证时，系统验证通过并自动开通，单次操作完成。
- **SC-005**: 商家能在商家端独立完成统编上传并保存成功；保存后运营后台可见。
- **SC-006**: 夜市（userType=3）账号与免用发票门店均不进入 ezPay 开通待办，待办列表 100% 由需开票且未就绪的门店构成。

## Assumptions

- ezPay 商店注册与申请为线下人工流程（在 ezPay 官网完成），平台不做注册类 API 对接；平台仅跟踪状态与录入凭证。
- 凭证验证使用 ezPay 测试环境（cinv.ezpay.com.tw）的只读查询接口，不产生真实发票或副作用。
- 本功能只覆盖"申请/开通管理 + 凭证录入验证"；订单完成时自动调用开票是后续独立功能，不在本期范围。
- ezPay 凭证按门店（pos_store）绑定：普通商家店铺与夜市下摊位门店各需一组凭证；夜市（userType=3）本身不开票，不纳入。
- 平台运营是免用发票标记与凭证录入的责任人（与"平台代申请"模型一致）；商家端仅负责上传统编。
- 现有 ezPay 开票/查询/加密工具类（EzPay、EzPayConfig、EzPayEncryptUtil）可复用，不在本期改动其内部实现。
- 门店列表中"实际卖货门店"的判定（依据 pos_store 的 isStall / isNightMarket 字段组合）在实现阶段对齐现有数据口径。
