spec.md 12 KB

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 字段组合)在实现阶段对齐现有数据口径。