Payment complete 后的 fan-out(Receipt / Invoice / Tip / Commission / Cash Drawer / Points) 分散在 4 个独立的调用站点,每个站点各自 inline 实现, 缺少统一的 payment-completed event bus。
4 个调用站点:
checkout-routes.js POST /cash — Cash 支付完成intent-routes.js POST /confirm — Flutter POS 回调确认pos-payment-service.js confirmPosPayment — POS Intent Mode 完成manual-entry-routes.js POST /manual-entry — 手工录入(平账用)后果: 新增 side-effect 时必须同步修改全部 4 处调用站点,遗漏风险极高。 事实上 已经存在不一致:Cash 路径缺 loyalty points,POS Intent 路径缺 commission, Manual entry 路径完全缺少 commission / loyalty / tip distribution。
| Side-Effect | checkout /cash | intent /confirm | POS Intent Mode | manual-entry |
|---|---|---|---|---|
| Tip Distribution | Yes | Yes | Yes | No |
| Commission Recording | Yes | Yes | No | No |
| Loyalty Points | No | Yes | No | No |
| Cash Drawer Update | Conditional | N/A (card) | N/A (card) | No |
| Guest Tagging | No | Yes | No | No |
| Receipt Generation | Manual API | Manual API | Manual API | Manual API |
flowchart TD
CHECKOUT["Checkout Initiated"]
PRICE["priceCalculator.calculateFullPrice"]
POS["POS Card Payment (CodePay P5)"]
CASH["Cash Payment (/cash)"]
SPLIT["Split Payment (splitPaymentService)"]
GIFTCARD["Gift Card Deduction"]
PAID["Payment Completed"]
RECEIPT["Receipt (receiptService)"]
INVOICE["Invoice (invoiceService)"]
TIP["Tip Distribution (tipDistributionService)"]
COMMISSION["Commission (commissionRecorder)"]
CASHDRAWER["Cash Drawer (cashDrawerService)"]
POINTS["Loyalty Points (loyaltyService)"]
DAYEND["Day-End Reconciliation"]
TAGGING["Guest Tagging (paymentHandler)"]
CHECKOUT -->|"P1 Initiate checkout"| PRICE
PRICE -->|"P2 POS card path"| POS
PRICE -->|"P3 Cash path"| CASH
PRICE -->|"P4 Split payment"| SPLIT
PRICE -->|"P5 Gift card deduct"| GIFTCARD
POS --> PAID
CASH --> PAID
SPLIT --> PAID
PAID -->|"P6 Generate receipt"| RECEIPT
PAID -->|"P7 Create/update invoice"| INVOICE
PAID -->|"P8 Distribute tip"| TIP
PAID -->|"P9 Record commission"| COMMISSION
PAID -->|"P10 Update cash drawer"| CASHDRAWER
PAID -->|"P11 Earn loyalty points"| POINTS
PAID -->|"P12 Guest tagging"| TAGGING
DAYEND -.->|"Day-end reconciliation"| CASHDRAWER
linkStyle 5 stroke:#22c55e,stroke-width:2px
linkStyle 6 stroke:#22c55e,stroke-width:2px
linkStyle 7 stroke:#22c55e,stroke-width:2px
linkStyle 8 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
linkStyle 9 stroke:#22c55e,stroke-width:2px
linkStyle 10 stroke:#22c55e,stroke-width:2px
linkStyle 11 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
linkStyle 12 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
linkStyle 13 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
linkStyle 14 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
| Status | Edge | 触发端 | 代码路径 | 审计说明 |
|---|---|---|---|---|
| ✓ | P1 Checkout → priceCalculator | Web Checkout |
api/payment/checkout-routes.js L352services/payment/priceCalculator.js
|
Checkout 路由调用 calculateFullPrice(),传入 items / discounts / tipAmount / taxRate。
税率通过 getTaxRateAsync() 从 DB 设置系统获取。
支持 package credit、subscription voucher、coupon 等多种折扣类型。
代码路径清晰,验证通过。
|
| ✓ | P2 Price → POS Payment | POS P5 Device |
services/pos-payment-service.js L49-120api/payment/intent-routes.js POST /confirm
|
两条 POS 路径: (a) POS Intent Mode:Web 创建 payment_request → POS 接受 → CodePay 支付 → POS 回调 confirmPosPayment。 (b) Legacy /confirm:Flutter App 直接调用 /confirm 传入 codepay_trans_no。 两条路径均有 transaction 和 history 写入,验证通过。 |
| ✓ | P3 Price → Cash Payment | Web Checkout |
api/payment/checkout-routes.js POST /cash (L727-933)
|
Cash 支付通过 calculateChange() 验证找零,
更新 transaction status 为 COMPLETED,
写入 cash_received / cash_change。
如提供 cash_drawer_session_id 则更新 cash_drawer_sessions 统计。
代码路径清晰,验证通过。
|
| ✓ | P4 Price → Split Payment | Web Checkout |
services/payment/splitPaymentService.jsapi/payment/split-payment-routes.js
|
SplitPaymentService 支持 4 种模式:Full / By Service / By Person / Manual。 计算拆分金额后创建独立 transaction,走正常的 POS 或 Cash 支付流程。 验证通过。 |
| ✓ | P5 Price → Gift Card Deduction | Web Checkout |
api/payment/checkout-routes.js L422-450 (FR-018)invoiceService.applyGiftCardDeduction()
|
Checkout 阶段直接扣减礼品卡余额。
discountType === 'gift_card' 的折扣项在 transaction 内循环调用
applyGiftCardDeduction。扣减失败回滚整个事务。
售卡已入账收入,兑换记为折扣(FR-018b),验证通过。
|
| ⚠ | P6 Payment Complete → Receipt | Manual (API) |
api/receipts.js POST /api/receiptsservices/payment/receiptService.js
|
Receipt 生成是 完全独立的 API 调用,
不是支付完成后自动触发。
前端需在支付成功回调中主动调用 POST /api/receipts。
POS Intent Mode 下,设备端通过 WebSocket 收到 receiptConfig
可触发打印,但后端无自动 receipt 创建逻辑。
风险:如前端遗漏调用,交易无收据记录。
|
| ✓ | P7 Payment Complete → Invoice | Checkout Transaction |
api/payment/checkout-routes.js L498-654services/payment/invoiceService.js
|
Invoice 在 checkout transaction 内创建(非 fan-out,而是同一事务的一部分)。 自动判定 single/group 类型,支持幂等(已有 invoice 则更新折扣信息)。 Cash / POS confirm 路径中 invoice 的 paid_amount 也会更新。 验证通过。 |
| ⚠ | P8 Payment Complete → Tip Distribution | checkout /cash, intent /confirm, POS Intent |
services/tipDistributionService.js distributeTipcheckout-routes.js L893-908intent-routes.js L341-358pos-payment-service.js L490-506
|
3 个支付完成路径均调用 distributeTip(),
条件是 tipAmount > 0 && appointmentId exists。
独立于 PAYROLL_V2_ENABLED,non-blocking (.catch)。
风险:manual-entry 路径完全缺失 tip distribution。
|
| ⚠ | P9 Payment Complete → Commission | checkout /cash, intent /confirm |
services/commission/commissionRecorder.js recordCommissionForPaymentcheckout-routes.js L855-891intent-routes.js L299-338
|
仅 checkout /cash 和 intent /confirm 两个路径调用 commissionRecorder。
受 PAYROLL_V2_ENABLED feature flag 控制。
风险:POS Intent Mode (pos-payment-service.js) 和 manual-entry 路径
均缺失 commission recording。
POS Intent Mode 是高频路径,佣金遗漏影响薪资准确性。
|
| ⚠ | P10 Payment Complete → Cash Drawer | checkout /cash (conditional) |
api/payment/checkout-routes.js L800-811services/cashDrawerService.js
|
Cash 支付路径中,仅当前端传入 cash_drawer_session_id 时
才更新 cash_drawer_sessions 的 cash_sales 和 total_transactions。
前置检查(FR-013)在 checkout 入口处通过 checkPreviousSessionsClosed
阻止前日未关账的交易。
风险:如前端未传 session_id,现金交易不会记入 cash drawer,
导致日结对账差异。
Card 支付路径无 cash drawer 交互(正确行为)。
|
| ⚠ | P11 Payment Complete → Loyalty Points | intent /confirm only |
api/payment/intent-routes.js L212-241services/loyalty/loyaltyService.js onPaymentCompleted
|
仅 intent /confirm 路径(Flutter POS 回调)调用
loyaltyService.onPaymentCompleted()。
checkout /cash 路径完全缺失 loyalty points 积累。
POS Intent Mode (pos-payment-service.js) 也缺失。
这意味着现金支付的客人不会获得积分,产生业务逻辑不一致。
条件:customer_id 和 tenantId 都非空。
|
| ✗ | P12 Day-End → Reconciliation | dayEndCloseoutService |
services/dayEndCloseoutService.jsservices/cashDrawerService.js
|
DayEndCloseoutService 协调日结流程(checklist / tip settlement / data locking),
但日结过程中发现的未记录佣金,是通过 commissionRecorder 补录的
而不是从 payment complete 事件自然流入。
day-end 与 cash drawer 之间是查询关系,不是事件触发:
日结读取 cash_drawer_sessions 的统计数据做对账,
但如果 P10 未触发(session_id 未传),对账数据本身就是不完整的。
整体对账链路依赖前置正确性,缺乏自愈机制。
|
6 项 side-effect: Tip Distribution / Commission / Loyalty Points / Cash Drawer / Guest Tagging / Receipt。 其中 Receipt 所有路径均为手动触发(不计入自动 fan-out)。 Invoice 在 checkout transaction 内同步创建(不属于 fan-out,而是事务内逻辑)。
checkout-routes.js POST /cash 完成支付后未调用
loyaltyService.onPaymentCompleted()。
对比 intent-routes.js POST /confirm 有完整的 loyalty 集成(L212-241)。
现金支付的客人不获得积分,与卡支付行为不一致。
建议:在 /cash 路径的 post-transaction 阶段添加 loyalty 调用, 与 /confirm 路径保持一致。
pos-payment-service.js confirmPosPayment 完成后
仅触发 tip distribution 和 appointment status sync,
缺失 commissionRecorder 和 loyaltyService 调用。
对比 checkout /cash 和 intent /confirm 均有 commission recording。
建议:在 confirmPosPayment 的 post-transaction 阶段 添加 commissionRecorder 和 loyaltyService 调用。
manual-entry-routes.js 创建已完成的 transaction,
但不触发 tip distribution / commission / loyalty / guest tagging 中的任何一项。
Manual entry 用于平账/补录,如果不触发佣金计算,
会导致员工薪资报表和实际业绩不一致。
建议:至少补齐 commission recording, 或在 day-end closeout 中增加 manual entry 的佣金补算逻辑。
所有支付路径的 receipt 生成均需前端调用 POST /api/receipts。
后端没有 auto-receipt 机制。如果用户在支付成功后关闭页面,
或 WebSocket 断开导致前端未收到成功回调,
该笔交易将没有任何收据记录。
建议:考虑在 payment complete 后自动创建 receipt 记录 (至少是 digital receipt),然后将打印/发送作为独立动作。
Cash 路径中 cash_drawer_session_id 是可选参数。
如果前端未传(例如未开启 cash drawer 功能的门店),
现金交易不会记入 cash_drawer_sessions,导致日结对账时
expected cash 与 actual cash 产生系统性偏差。
建议:后端应自动查找当前门店的 active session, 而不是依赖前端传入。
当前 4 个调用站点各自 inline 实现 side-effects,违反 DRY 原则且极易遗漏。
建议引入统一的 onPaymentCompleted(req, transaction) 函数或事件总线:
intent-routes.js L212-241 调用 loyaltyService.onPaymentCompleted()
触发积分累计和会员等级升级。这是 Payment 子图与 Retention 子图
的主要连接点。
注意:此连接点仅在 intent /confirm 路径存在, cash 路径和 POS Intent 路径断裂。
Commission recording 将支付数据导入薪资系统(commissionRecorder),
Cash drawer 更新影响日结对账(dayEndCloseoutService)。
这两条边是 Payment 子图与 Operations 子图
的连接点。
从 Delivery 子图 的 D9 (Completed → Payment 出口)
进入本子图。服务完成后,前端导航到 Checkout 页面,
触发 POST /api/payment/checkout 创建 PENDING transaction。