# 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 作废接口返回为准)。 --- ### User Story 6 - 客户捐赠发票给社福机构 (Priority: P2) 客户开票时可选第三种类型「捐赠发票」:从平台提供的社福机构清单中选择机构(或手输捐赠码),系统校验捐赠码后通过 ezPay 以 LoveCode 开立捐赠发票(PrintFlag=N),发票捐给所选机构。客户无需填姓名/邮箱/载具/统编。 **Why this priority**: 捐赠属公益场景、非外卖开票主路径,复用已建开票链路做增量即可,故 P2。 **Independent Test**: 已完成订单选「捐赠发票」→ 从机构清单选「创世基金会」→ 开立成功 → 发票记录捐赠码 919 与机构名、无纸本凭证。 **Acceptance Scenarios:** 1. **Given** 订单已完成且门店可开票,**When** 客户选「捐赠发票」并从机构清单选择一家机构,**Then** 系统以该机构捐赠码调 ezPay 开立(LoveCode 填码、CarrierType 空、PrintFlag=N),发票记录捐赠码与机构名,客户看到「已捐赠给 XXX」。 2. **Given** 客户选「捐赠发票」并手输捐赠码,**When** 码格式非法或不存在,**Then** 提交前拦截并提示,不调 ezPay。 3. **Given** 捐赠发票开立成功,**When** 客户/商家/运营查看,**Then** 显示捐赠码、捐赠机构名与「已捐赠」标识,且无发票条码/二维码(PrintFlag=N 合规不索取纸本)。 --- ### Edge Cases - **同一订单重复申请开票**:已成功开票的订单不允许再次开票(除非先作废);需防止并发点击产生重复发票。 - **ezPay 开票接口网络异常**:不开立、订单标记失败、不产生半成品发票;提示稍后重试。 - **ezPay 回应业务错误**(金额不平、字轨用尽等):记录错误码与信息,订单标记失败、可重试。 - **客户邮箱/载具填错导致发票无法送达**:发票在 ezPay 侧已开立成功,记录发票号,提示客户联系商家;本期不做载体自动更正。 - **订单退款/取消后已开发票**:本期不自动作废,由商家按需手动作废(US5)。 - **夜市(userType=3)门店与免用发票门店**:不提供开票入口(与 009 一致)。 - **门店 ezPay 被停用(009 toggleEnable 关)**:开票时识别为不可开票,提示门店发票功能暂停。 - **捐赠码超过 ezPay 接受长度**:财政部登记允许 3-8 位,但 ezPay `invoice_issue`/`checkLoveCode` 限 3-7 位(Int(7));导入清单时对 >7 位的码标记不可用,客户手输 8 位码会被 ezPay 以 CLC10002 拒,提示「该捐赠码暂不支持」。 - **客户所选机构不在本地清单**:手输码走 ezPay `checkLoveCode` 验真(只回 IsExist Y/N,不回机构名);机构名未知时显示「捐赠码有效(机构未知)」。 - **捐赠发票无纸本凭证**:PrintFlag=N 时 ezPay 不返回 BarCode/QRcode;不能复用个人发票的条码展示模板,捐赠发票详情需单独模板(结构化信息 + 「已捐赠给 XXX」)。 - **捐赠与载具互斥**:选捐赠时载具/邮箱/统编字段隐藏;若请求同时带 CarrierType 与 LoveCode,ezPay 校验拦截。 ## 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 由客户端团队另行实现,不在本期。 - **FR-013**: 系统必须存储财政部下发的捐赠机构清单(捐赠码、机构全名、机构简称、统编、县市),并支持平台同步更新(CSV 导入)。 - **FR-014**: 系统必须提供客户端可调用的接口,把捐赠机构列表返回给前端,供客户选择机构并自动回填捐赠码,支持按机构名/简称搜索。 - **FR-015**: 客户开票时可选「捐赠发票」,填入捐赠码(3-7 位数字),系统校验格式与存在性(本地清单或 ezPay `checkLoveCode`)后,调 ezPay 以 LoveCode 开立(Category=B2C、CarrierType 空、PrintFlag=N),成功后记录捐赠码与机构名。 - **FR-016**: 捐赠发票不展示纸本凭证(码图为空),客户端/管理端展示「已捐赠给 {机构}」;载具与捐赠互斥(同一张发票不可既存载具又捐赠)。 ### Key Entities *(include if feature involves data)* - **订单发票(新增,与订单 1:1)**:记录一笔订单的电子发票信息:发票类型(B2C/B2B)、买方名称、买方统编、接收邮箱、载具类型与号码、发票号码、防伪随机码、发票查看凭证、开票状态(未开 / 已开 / 失败 / 作废)、失败原因、申请时间、开立时间、作废时间。 - **订单(已有)**:开票的来源;订单完成后可发起开票;一笔订单对应一张有效发票(作废后可重开)。 - **门店 ezPay 配置(009 已有)**:开票时读取门店的 ezPay 凭证(商店代号 / HashKey / HashIV),并判断门店是否可开票(已开通且启用)。 - **ezPay 加值服务平台(外部)**:实际开立 / 作废发票的外部系统,本期通过其开立 / 作废接口与之交互。 - **捐赠机构字典表(新增,US6)**:财政部社福机构清单。字段:捐赠码(VARCHAR 保留前导零)、机构全名、机构简称(可空)、统编、县市(可空)、是否启用、同步时间。供客户端选择器与开票时回显机构名。 - **订单发票(扩展,US6)**:新增 `love_code`(捐赠码,开捐赠发票时填)与 `love_org_name`(捐赠机构名,开票时从字典快照,便于历史展示)。 ## Success Criteria *(mandatory)* ### Measurable Outcomes - **SC-001**: 客户对一笔已完成订单,能在 1 分钟内、3 次操作内完成发票申请并拿到发票查看凭证。 - **SC-002**: 门店为「免用发票」或 ezPay 未开通/未启用时,客户 100% 看不到开票入口(无法发起开票)。 - **SC-003**: 客户填错统编格式时,系统 100% 在调用开票接口前拦截并提示。 - **SC-004**: 同一订单不会因重复点击或并发申请产生 2 张及以上发票。 - **SC-005**: 开票失败(网络或业务错误)的订单,客户/运营可在不改动订单状态的前提下重新开票成功。 - **SC-006**: 客户、商家、运营三方都能在各自入口查看到同一订单的发票号与开票状态,且一致。 - **SC-007**: 客户在捐赠发票流程中能从机构列表选择机构并在 3 次操作内完成开票,发票正确捐给所选机构。 - **SC-008**: 机构列表接口返回完整清单(含搜索),客户端可直接渲染选择器并自动回填捐赠码。 ## Assumptions - 「订单完成」由现有订单状态机判定(订单到达可开票的终态),本期不改订单状态机;具体终态口径在实现阶段对齐现有逻辑。 - 开票凭证(商店代号 / HashKey / HashIV)由 009 的门店 ezPay 配置提供;仅「已开通(status=2)且启用(enabled=1)」的门店可开票。 - 复用已实现的 ezPay 工具类(EzPay / EzPayConfig / EzPayEncryptUtil),本期不改其内部。 - 餐饮主走 B2C 即时开立(Status=1);B2B 需统编;载具为 P2 体验增强。 - 发票作废受台湾法规时点限制,系统以 ezPay 作废接口返回为准判定是否可作废,不在本地做时限计算。 - 客户端前端 UI(订单详情开票入口、发票查看)不在本期实现范围:本期交付后端开票/查询 API(供客户端调用)+ 管理端(商家/平台后台)查看/重试/作废;客户端 UI 及其 i18n 由客户端团队后续对接。 - 本期不含:发票折让、字轨管理、批次开立、发票中奖通知——这些属后续独立功能。 - 捐赠发票(LoveCode)作为 US6 增量纳入本期:客户可选「捐赠发票」类型,发票捐给社福机构。捐赠机构清单来自财政部电子发票整合服务平台(CSV 已下载于 `specs/010-order-invoice/xca.csv`,约 2016 家),需定期同步;ezPay 不提供机构列表、`checkLoveCode` 只回 IsExist Y/N 不回机构名(详见 memory `reference-lovecode-donation`)。 - 捐赠码长度以 ezPay 的 3-7 位为准(非财政部登记的 3-8 位):导入清单时过滤 >7 位的码或标记不可用。 - 捐赠机构 CSV 数据特征:UTF-8 编码;记录以 CR(`\r`)分隔非 CRLF,导入需先归一化换行;捐赠码含前导零(如 `0329`/`0095`/`000`),须 VARCHAR 存储不可用整型;机构简称与县市可空。 - 捐赠发票 PrintFlag=N:ezPay 不返回 BarCode/QRcode 码图,遵循 `reference-tw-einvoice-concepts` 的码图规则(仅 PrintFlag=Y 才有码图)。 - 发票金额不含运费:运费由用户承担(付给配送方),非商家商品收入;商家发票仅就商品销售开立。订单构成 `amount = 商品 − 各项优惠 + 运费`,故发票金额 = `amount − 运费`,各类优惠已含在 amount 内无需逐项区分。 - 四语言 i18n 覆盖所有新增面向用户文字,遵循项目既有 i18n 规范。