# Feature Specification: 订单 ezPay 电子发票开立 **Feature Branch**: 不新建分支(在当前 test 分支开发) **Created**: 2026-06-16 **Status**: Draft **Input**: User description: "订单完成后客户要开发票(ezPay 电子发票开票)。基于已完成的 009-ezpay-invoice-onboarding(门店 ezPay 开通管理 + 凭证录入),复用已有 EzPay 开票工具类,在订单完成时为已开通且启用的门店开立电子发票。不创建分支,在当前 test 分支开发。" ## User Scenarios & Testing *(mandatory)* ### User Story 1 - 客户在订单完成后申请个人电子发票 (Priority: P1) 客户下单并在订单完成后,可在订单详情发起开票,选择开立个人(B2C)电子发票并填写接收邮箱;系统通过门店已配置的 ezPay 凭证即时开立电子发票,返回发票号码与发票查看凭证,客户可查看/下载。仅「已开通且启用」的门店能为客户开票;免用发票门店不提供开票入口。 **Why this priority**: 这是「客户能开到发票」的核心价值,也是 ezPay 开通管理(009)真正落地的出口。没有它,门店开通 ezPay 没有任何意义。 **Independent Test**: 对一笔已完成订单(所属门店已开通且启用 ezPay)申请个人发票 → 系统开立成功 → 订单显示发票号、客户可查看发票。 **Acceptance Scenarios**: 1. **Given** 订单已完成且所属门店 ezPay 已开通且启用,**When** 客户在订单详情点「申请发票」、选个人发票并填接收邮箱,**Then** 系统调 ezPay 即时开立,订单记录发票号与防伪随机码,客户拿到发票查看凭证。 2. **Given** 订单所属门店为「免用发票」或 ezPay 未开通/未启用,**When** 客户查看订单,**Then** 不显示开票入口(或提示该门店暂不支持开票)。 3. **Given** 订单已成功开票,**When** 客户再次查看订单,**Then** 显示发票号与发票链接,且无法再次申请开票。 --- ### User Story 2 - 客户申请公司(B2B)电子发票并填写统一编号 (Priority: P1) 客户可选择开立公司(B2B)发票,填写公司名称与统一编号(统编,8 码);系统校验统编格式后调 ezPay 开立三联式发票,发票上印有买方统编。 **Why this priority**: 外卖/餐饮场景下 B2B 报账是刚需(统编为标配),与个人开票同为核心开票路径。 **Independent Test**: 对一笔已完成订单选公司发票、填公司名 + 合法统编 → 开立成功 → 发票记录买方统编。 **Acceptance Scenarios**: 1. **Given** 订单已完成且门店可开票,**When** 客户选公司发票并填合法统编(8 码)与公司名,**Then** 开立成功,发票记录买方统编。 2. **Given** 客户填了格式非法的统编,**When** 提交申请,**Then** 校验失败并提示,不调用开票接口。 --- ### User Story 3 - 客户选择电子发票载具 (Priority: P2) 客户开立个人发票时,可选择将发票存入载具(手机条码 / 自然人凭证 / ezPay 会员载具);不选则默认以邮箱接收。 **Why this priority**: 台湾电子发票的载具是常态体验,但并非「开到发票」的必要前提,属体验增强,故 P2。 **Independent Test**: 选手机条码载具开票 → 发票存入该载具,不另行印发邮箱 PDF。 **Acceptance Scenarios**: 1. **Given** 客户开个人发票,**When** 选手机条码载具并填条码,**Then** 校验条码合法后开立,发票存入该载具。 2. **Given** 客户开个人发票,**When** 选自然人凭证载具并填凭证,**Then** 校验后开立,发票存入该载具。 --- ### User Story 4 - 商家与平台查看订单发票、失败重试 (Priority: P2) 商家在商家端、运营在平台后台可查看订单的开票状态(未开 / 已开 / 失败 / 作废)与发票号;开票失败的订单可由运营重新触发开票。 **Why this priority**: 运营与商家需要掌握开票情况、处理失败单,但依赖开票主链路先打通,故 P2。 **Independent Test**: 一笔开票失败的订单,运营在后台点「重新开票」 → 成功后状态变已开。 **Acceptance Scenarios**: 1. **Given** 订单曾开票失败,**When** 运营/商家查看,**Then** 显示失败状态与失败原因。 2. **Given** 一笔开票失败的订单,**When** 运营点重新开票,**Then** 系统再次调 ezPay 开立,成功则更新发票号与状态。 --- ### User Story 5 - 商家作废已开立的发票 (Priority: P3) 商家/运营可对已开立的发票发起作废(受台湾法规「奇数月 14 日前可作废前两月发票」限制),作废成功后订单可重新开票。 **Why this priority**: 作废属售后补救,频率低且有严格时点限制,属辅助功能,故 P3。 **Independent Test**: 对一张本月开具的发票点作废 → 调 ezPay 作废成功 → 发票状态变作废、订单可重开。 **Acceptance Scenarios**: 1. **Given** 一张开立且在可作废期限内的发票,**When** 商家发起作废,**Then** 系统调 ezPay 作废,发票标记作废。 2. **Given** 已超过可作废期限的发票,**When** 商家发起作废,**Then** 提示不可作废并说明原因(以 ezPay 作废接口返回为准)。 --- ### Edge Cases - **同一订单重复申请开票**:已成功开票的订单不允许再次开票(除非先作废);需防止并发点击产生重复发票。 - **ezPay 开票接口网络异常**:不开立、订单标记失败、不产生半成品发票;提示稍后重试。 - **ezPay 回应业务错误**(金额不平、字轨用尽等):记录错误码与信息,订单标记失败、可重试。 - **客户邮箱/载具填错导致发票无法送达**:发票在 ezPay 侧已开立成功,记录发票号,提示客户联系商家;本期不做载体自动更正。 - **订单退款/取消后已开发票**:本期不自动作废,由商家按需手动作废(US5)。 - **夜市(userType=3)门店与免用发票门店**:不提供开票入口(与 009 一致)。 - **门店 ezPay 被停用(009 toggleEnable 关)**:开票时识别为不可开票,提示门店发票功能暂停。 ## Requirements *(mandatory)* ### Functional Requirements - **FR-001**: 系统必须能在订单完成后,允许客户为该订单申请电子发票(仅当订单所属门店 ezPay 已开通且启用)。 - **FR-002**: 系统必须支持客户开立个人(B2C)电子发票,含接收邮箱,并通过 ezPay 即时开立接口完成开立。 - **FR-003**: 系统必须支持客户开立公司(B2B)电子发票,含买方名称与统一编号(统编,8 码),并在提交前校验统编格式。 - **FR-004**: 系统必须在开票成功后记录发票号码、防伪随机码等 ezPay 回应信息并关联到订单,供客户与商家查看。 - **FR-005**: 系统必须避免同一订单重复开票——已成功开票的订单不允许再次开票,除非该发票已作废。 - **FR-006**: 系统必须在门店为「免用发票」或 ezPay 未开通/未启用时,不向客户提供开票入口。 - **FR-007**: 开票失败(网络异常或 ezPay 业务错误)时,系统必须标记订单开票失败并记录原因,不改变订单本身状态,并允许后续重新开票。 - **FR-008**: 系统必须支持客户选择电子发票载具(手机条码 / 自然人凭证 / ezPay 会员载具);不选则以邮箱接收。 - **FR-009**: 商家端与平台后台必须能查看订单开票状态(未开 / 已开 / 失败 / 作废)与发票号;运营可对失败订单重新开票。 - **FR-010**: 商家/运营必须能对已开立且在可作废期限内的发票发起作废,作废成功后订单可重新开票。 - **FR-011**: 系统必须复用已实现的 ezPay 开票工具类(开立 / 查询 / 作废),不在本期改动其内部实现。 - **FR-012**: 商家端、平台后台新增的面向用户文字必须实现四语言(vi / zh / tw / en)国际化;客户端 UI 的 i18n 由客户端团队另行实现,不在本期。 ### Key Entities *(include if feature involves data)* - **订单发票(新增,与订单 1:1)**:记录一笔订单的电子发票信息:发票类型(B2C/B2B)、买方名称、买方统编、接收邮箱、载具类型与号码、发票号码、防伪随机码、发票查看凭证、开票状态(未开 / 已开 / 失败 / 作废)、失败原因、申请时间、开立时间、作废时间。 - **订单(已有)**:开票的来源;订单完成后可发起开票;一笔订单对应一张有效发票(作废后可重开)。 - **门店 ezPay 配置(009 已有)**:开票时读取门店的 ezPay 凭证(商店代号 / HashKey / HashIV),并判断门店是否可开票(已开通且启用)。 - **ezPay 加值服务平台(外部)**:实际开立 / 作废发票的外部系统,本期通过其开立 / 作废接口与之交互。 ## Success Criteria *(mandatory)* ### Measurable Outcomes - **SC-001**: 客户对一笔已完成订单,能在 1 分钟内、3 次操作内完成发票申请并拿到发票查看凭证。 - **SC-002**: 门店为「免用发票」或 ezPay 未开通/未启用时,客户 100% 看不到开票入口(无法发起开票)。 - **SC-003**: 客户填错统编格式时,系统 100% 在调用开票接口前拦截并提示。 - **SC-004**: 同一订单不会因重复点击或并发申请产生 2 张及以上发票。 - **SC-005**: 开票失败(网络或业务错误)的订单,客户/运营可在不改动订单状态的前提下重新开票成功。 - **SC-006**: 客户、商家、运营三方都能在各自入口查看到同一订单的发票号与开票状态,且一致。 ## Assumptions - 「订单完成」由现有订单状态机判定(订单到达可开票的终态),本期不改订单状态机;具体终态口径在实现阶段对齐现有逻辑。 - 开票凭证(商店代号 / HashKey / HashIV)由 009 的门店 ezPay 配置提供;仅「已开通(status=2)且启用(enabled=1)」的门店可开票。 - 复用已实现的 ezPay 工具类(EzPay / EzPayConfig / EzPayEncryptUtil),本期不改其内部。 - 餐饮主走 B2C 即时开立(Status=1);B2B 需统编;载具为 P2 体验增强。 - 发票作废受台湾法规时点限制,系统以 ezPay 作废接口返回为准判定是否可作废,不在本地做时限计算。 - 客户端前端 UI(订单详情开票入口、发票查看)不在本期实现范围:本期交付后端开票/查询 API(供客户端调用)+ 管理端(商家/平台后台)查看/重试/作废;客户端 UI 及其 i18n 由客户端团队后续对接。 - 本期不含:发票折让、字轨管理、批次开立、发票中奖通知——这些属后续独立功能。 - 捐赠码(LoveCode)不在本期范围:客户开票仅支持载具或邮箱接收,不做捐赠。 - 发票金额不含运费:运费由用户承担(付给配送方),非商家商品收入;商家发票仅就商品销售开立。订单构成 `amount = 商品 − 各项优惠 + 运费`,故发票金额 = `amount − 运费`,各类优惠已含在 amount 内无需逐项区分。 - 四语言 i18n 覆盖所有新增面向用户文字,遵循项目既有 i18n 规范。