Przeglądaj źródła

feat(omgpay): limit checkout to credit and apple pay

qmj 2 tygodni temu
rodzic
commit
586c40c860

+ 3 - 5
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentFormFactory.java

@@ -44,15 +44,13 @@ public class OmgPaymentFormFactory {
         fields.put("ItemName", "Order " + safeOrderId);
         fields.put("ReturnURL", properties.requireSafeReturnUrl());
         fields.put("OrderResultURL", properties.requireSafeOrderResultUrl());
-        fields.put("ClientRedirectURL", properties.requireSafeClientRedirectUrl());
         fields.put("ClientBackURL", properties.requireSafeClientBackUrl(merchantTradeNo));
-        fields.put("ChoosePayment", "ALL");
+        // OMG documents Apple Pay under Credit. UnionPay=2 hides the remaining UnionPay option.
+        fields.put("ChoosePayment", "Credit");
+        fields.put("UnionPay", "2");
         fields.put("EncryptType", "1");
         fields.put("InvoiceMark", "N");
         fields.put("NeedExtraPaidInfo", "Y");
-        fields.put("ExpireDate", "1");
-        fields.put("StoreExpireDate", "30");
-        fields.put("BarcodeATMExpireDate", "1");
         Map<String, String> signedFields = Collections.unmodifiableMap(new LinkedHashMap<>(fields));
         fields.put("CheckMacValue", signer.sign(signedFields, hashKey, hashIv));
         Map<String, String> responseFields = Collections.unmodifiableMap(new LinkedHashMap<>(fields));

+ 0 - 14
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentProperties.java

@@ -10,7 +10,6 @@ import java.net.URI;
 public class OmgPaymentProperties {
     private String returnUrl;
     private String orderResultUrl;
-    private String clientRedirectUrl;
     private String clientBackUrl;
     private String appReturnUrl;
 
@@ -30,14 +29,6 @@ public class OmgPaymentProperties {
         this.orderResultUrl = orderResultUrl;
     }
 
-    public String getClientRedirectUrl() {
-        return clientRedirectUrl;
-    }
-
-    public void setClientRedirectUrl(String clientRedirectUrl) {
-        this.clientRedirectUrl = clientRedirectUrl;
-    }
-
     public String getClientBackUrl() {
         return clientBackUrl;
     }
@@ -62,11 +53,6 @@ public class OmgPaymentProperties {
         return requireSafeCallbackUrl(orderResultUrl, "/pay/omg/result", "OrderResultURL");
     }
 
-    public String requireSafeClientRedirectUrl() {
-        return requireSafeCallbackUrl(clientRedirectUrl,
-                "/pay/omg/payment-info-result", "ClientRedirectURL");
-    }
-
     public String requireSafeClientBackUrl(String merchantTradeNo) {
         if (merchantTradeNo == null || !merchantTradeNo.matches("[A-Z0-9]{1,20}")) {
             throw new IllegalArgumentException("OMG ClientBackURL trade number is invalid");

+ 0 - 1
ruoyi-admin/src/main/resources/application.yml

@@ -45,7 +45,6 @@ omg:
 omgpay:
   return-url: https://foodieapi.waimai-paotui.com/pay/omg/notify
   order-result-url: https://foodieapi.waimai-paotui.com/pay/omg/result
-  client-redirect-url: https://foodieapi.waimai-paotui.com/pay/omg/payment-info-result
   client-back-url: https://foodieapi.waimai-paotui.com/pay/omg/back
   # 必须配置为已完成 iOS Universal Link / Android App Link 关联的 HTTPS 地址。
   app-return-url: ${OMG_APP_RETURN_URL:}

+ 11 - 8
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentFormFactoryTest.java

@@ -22,7 +22,7 @@ import static org.mockito.Mockito.when;
 
 class OmgPaymentFormFactoryTest {
     @Test
-    void buildsExactStageOnlySignedForm() {
+    void buildsSignedFormThatOnlyOffersCreditAndApplePay() {
         OmgPaymentProperties properties = properties();
         OmgCheckMacSigner signer = mock(OmgCheckMacSigner.class);
         when(signer.sign(anyMap(), eq("KEY"), eq("IV"))).thenReturn("A".repeat(64));
@@ -34,22 +34,26 @@ class OmgPaymentFormFactoryTest {
         assertEquals(OmgPaymentFormFactory.STAGE_GATEWAY_URL, form.gatewayUrl());
         assertEquals(Set.of("MerchantID", "MerchantTradeNo", "MerchantTradeDate", "PaymentType",
                 "TotalAmount", "TradeDesc", "ItemName", "ReturnURL", "ChoosePayment", "EncryptType",
-                "InvoiceMark", "NeedExtraPaidInfo", "ExpireDate", "StoreExpireDate",
-                "BarcodeATMExpireDate", "OrderResultURL", "ClientRedirectURL", "ClientBackURL",
+                "InvoiceMark", "NeedExtraPaidInfo", "UnionPay", "OrderResultURL", "ClientBackURL",
                 "CheckMacValue"), form.fields().keySet());
         assertEquals("2026/08/13 15:30:23", form.fields().get("MerchantTradeDate"));
-        assertEquals("ALL", form.fields().get("ChoosePayment"));
+        assertEquals("Credit", form.fields().get("ChoosePayment"));
+        assertEquals("2", form.fields().get("UnionPay"));
         assertEquals("Y", form.fields().get("NeedExtraPaidInfo"));
         assertEquals("https://foodieapi.waimai-paotui.com/pay/omg/result",
                 form.fields().get("OrderResultURL"));
-        assertEquals("https://foodieapi.waimai-paotui.com/pay/omg/payment-info-result",
-                form.fields().get("ClientRedirectURL"));
         assertEquals("https://foodieapi.waimai-paotui.com/pay/omg/back?merchantTradeNo=OMG12345678901234567",
                 form.fields().get("ClientBackURL"));
+        assertFalse(form.fields().containsKey("ClientRedirectURL"));
+        assertFalse(form.fields().containsKey("ExpireDate"));
+        assertFalse(form.fields().containsKey("StoreExpireDate"));
+        assertFalse(form.fields().containsKey("BarcodeATMExpireDate"));
         assertEquals("Foodie order DDscript", form.fields().get("TradeDesc"));
         assertFalse(form.fields().get("ItemName").contains("|"));
         assertTrue(form.fields().get("ItemName").length() <= 120);
-        verify(signer).sign(org.mockito.ArgumentMatchers.argThat(fields -> fields.size() == 18
+        verify(signer).sign(org.mockito.ArgumentMatchers.argThat(fields -> fields.size() == 15
+                && "Credit".equals(fields.get("ChoosePayment"))
+                && "2".equals(fields.get("UnionPay"))
                 && !fields.containsKey("CheckMacValue")), eq("KEY"), eq("IV"));
         assertThrows(UnsupportedOperationException.class, () -> form.fields().put("extra", "value"));
     }
@@ -78,7 +82,6 @@ class OmgPaymentFormFactoryTest {
         OmgPaymentProperties properties = new OmgPaymentProperties();
         properties.setReturnUrl("https://foodieapi.waimai-paotui.com/pay/omg/notify");
         properties.setOrderResultUrl("https://foodieapi.waimai-paotui.com/pay/omg/result");
-        properties.setClientRedirectUrl("https://foodieapi.waimai-paotui.com/pay/omg/payment-info-result");
         properties.setClientBackUrl("https://foodieapi.waimai-paotui.com/pay/omg/back");
         properties.setAppReturnUrl("https://link.waimai-paotui.com/payment/omg");
         return properties;

+ 4 - 5
specs/016-omg-payment/contracts/api.md

@@ -14,8 +14,8 @@
 - **URL**:测试 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`;正式 `https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5`
 - **Method/CT**:`POST`,`application/x-www-form-urlencoded`
 - **方式**:后端组参 + 计算 CheckMacValue 返回 form 字段,**前端 Form Post** 跳转 OMG 收银台(不可 iframe;iOS 不可另开窗)
-- **必填请求参数**:`MerchantID`、`MerchantTradeNo`(=OMG+ddId)、`MerchantTradeDate`(yyyy/MM/dd HH:mm:ss)、`PaymentType=aio`、`TotalAmount`(整数元)、`ReturnURL`、`ChoosePayment=ALL`、`CheckMacValue`、`EncryptType=1`
-- **常用可选**:`TradeDesc`、`ItemName`(# 分隔)、`OrderResultURL`、`PaymentInfoURL`、`ClientRedirectURL`、`NeedExtraPaidInfo`(建议 Y,供退款查 gwsr)、`CustomField1–4`、`Language`
+- **必填请求参数**:`MerchantID`、`MerchantTradeNo`(=OMG+ddId)、`MerchantTradeDate`(yyyy/MM/dd HH:mm:ss)、`PaymentType=aio`、`TotalAmount`(整数元)、`ReturnURL`、`ChoosePayment=Credit`、`CheckMacValue`、`EncryptType=1`
+- **本项目固定参数**:`UnionPay=2`(隐藏银联)、`InvoiceMark=N`、`NeedExtraPaidInfo=Y`、`OrderResultURL`、`ClientBackURL`;新订单不得传 `PaymentInfoURL`、`ClientRedirectURL` 或延期缴费期限参数。
 - **固定**:`InvoiceMark=N`(发票走 ezPay);`ReturnURL ≠ OrderResultURL`;`MerchantTradeNo` 全平台唯一
 
 ### A2. 付款结果回调 — ReturnURL(盘合 → 我方,US2)
@@ -52,7 +52,7 @@
 
 - 入参:`orderid`(= ddId)
 - 逻辑:校验订单归属/金额/未支付 → 查门店启用凭证 → 生成 `MerchantTradeNo` → 组参 + CheckMacValue → 落 `pos_order_omg_payment`(pay_status=0) + 更新 `pos_order.pay_type="7"`/`pay_url`
-- 返回:form 字段 `{ gatewayUrl, MerchantID, MerchantTradeNo, MerchantTradeDate, PaymentType=aio, TotalAmount, ReturnURL, ChoosePayment=ALL, EncryptType=1, ItemName, CheckMacValue, ... }`,前端构建隐藏 form 自动 submit 到 `gatewayUrl`
+- 返回:form 字段 `{ gatewayUrl, MerchantID, MerchantTradeNo, MerchantTradeDate, PaymentType=aio, TotalAmount, ReturnURL, ChoosePayment=Credit, UnionPay=2, EncryptType=1, ItemName, CheckMacValue, ... }`,前端构建隐藏 form 自动 submit 到 `gatewayUrl`
 
 ### B2. OMG 支付结果回调 — `POST /pay/omg/notify`(US2,@Anonymous)
 
@@ -114,8 +114,7 @@
 
 ### C1. ChoosePayment(请求参数,US1)
 
-本期固定 `ALL`(聚合全部)。可细分值:`Credit`(信用卡/銀聯/Apple Pay)、`ATM`(FIRST/CHINATRUST/UBOT/KGI)、`CVS`(CVS/FAMILY/IBON/HILIFE)、`BarcodeATM`(CHINATRUST)、`AFTEE`。
-⚠️ **Apple Pay 不是独立 ChoosePayment**,归在 `Credit` 下(付款页内选);回覆 PaymentType 同为 `Credit_CreditCard`,**仅凭 PaymentType 无法区分信用卡与 Apple Pay**(本期不区分)。
+本期固定 `Credit`,并传 `UnionPay=2` 隐藏银联,只保留普通信用卡与 Apple Pay。OMG 官方将 Apple Pay 归在 `Credit` 下;回覆 PaymentType 同为 `Credit_CreditCard`,仅凭 PaymentType 无法区分两者(本期不区分)。
 
 ### C2. 回覆 PaymentType(回调 `PaymentType`,落 `pos_order_omg_payment.pay_type`)
 

+ 1 - 1
specs/016-omg-payment/data-model.md

@@ -24,7 +24,7 @@
 | merchant_id | VARCHAR(10) UK | OMG 商店代号 MerchantID;回调反查门店凭证,非空时唯一 |
 | hash_key | VARCHAR(64) | OMG HashKey |
 | hash_iv | VARCHAR(64) | OMG HashIV |
-| enabled_payments | VARCHAR(50) | 启用支付方式(本期固定 ALL,字段保留) |
+| enabled_payments | VARCHAR(50) | 历史配置字段(运行时固定 Credit,不再决定收银台方式) |
 | apply_time | DATETIME | 提交申请时间(0→1) |
 | approved_time | DATETIME | 开通时间(→2) |
 | last_verify_result | VARCHAR(240) | 最近一次凭证验证结果 |

+ 13 - 12
specs/016-omg-payment/frontend-integration.md

@@ -13,7 +13,7 @@ OMG(欧买尬/FunPoint)是**网页托管式支付网关**(AIO = All-In-One
 | LINE Pay | 有 LINE App | 唤起 LINE App 内 | deep link 唤起 |
 | **OMG / FunPoint** | **没有 App** | **OMG 的网页收银台** | **form post 跳网页** |
 
-后端 `ChoosePayment=ALL` 已把 **信用卡 / Apple Pay / ATM / 超商 / AFTEE** 全聚合在一个收银台里,用户在那个网页上自己选方式付款。**前端不需要集成任何支付 SDK,只需要把用户「跳过去」再「接回来」。**
+后端固定使用 `ChoosePayment=Credit`、`UnionPay=2`,OMG 收银台只提供 **信用卡与 Apple Pay**。**前端不需要集成任何支付 SDK,只需要把用户「跳过去」再「接回来」。**
 
 ---
 
@@ -32,7 +32,7 @@ OMG(欧买尬/FunPoint)是**网页托管式支付网关**(AIO = All-In-One
         │  omgPay.html 自动 submit 隐藏表单 → 跳转
-[OMG 收银台网页]  用户选支付方式(信用卡/Apple Pay/ATM/超商)并付款
+[OMG 收银台网页]  用户使用信用卡或 Apple Pay 付款
         │  ③ 付款后 OMG 把 web-view 跳回 OrderResultURL
         │     后端 /pay/omg/return 302 → 前端结果页?ddId=xxx
@@ -55,7 +55,7 @@ OMG(欧买尬/FunPoint)是**网页托管式支付网关**(AIO = All-In-One
 | 1 | 放一个 `omgPay.html` 到 `static/`,作为 web-view 收银台跳转页 | uni-app 工程 |
 | 2 | 发起支付:调 `/pay/omg/create` → 拼 URL → 打开 web-view | 发起支付页 |
 | 3 | 支付结果页:接收 `ddId` → 查状态 → 展示成功/失败/取号 | 支付结果页 |
-| 4 | ATM/超商取号展示页:调 `/pay/omg/paymentInfo/{orderid}` 展示虚帐/缴费码 | 取号页(可与结果页合并) |
+| 4 | 不再实现 ATM/超商取号展示;后端旧接口仅兼容变更前交易 | 无新增页面 |
 | 5 | (可选)取消订单退款:调 `/pay/omg/refund` | 订单取消逻辑 |
 | 6 | 告诉后端两个前端地址(见 §6 配置依赖) | 沟通 |
 | 7 | 所有面向用户文字走 i18n 4 语言 | zh/tw/en/vi |
@@ -99,7 +99,8 @@ Header: token: eyJhbGciOi...
     "TradeDesc": "food order DD202608060001",
     "ItemName": "order DD202608060001",
     "ReturnURL": "https://api.xxx.com/pay/omg/notify",
-    "ChoosePayment": "ALL",
+    "ChoosePayment": "Credit",
+    "UnionPay": "2",
     "EncryptType": "1",
     "InvoiceMark": "N",
     "NeedExtraPaidInfo": "Y",
@@ -136,9 +137,9 @@ Header: token: eyJhbGciOi...
 
 ---
 
-### 3.3 ATM/超商取号查询 — `GET /pay/omg/paymentInfo/{orderid}`
+### 3.3 历史 ATM/超商取号查询(新客户端不得调用)
 
-**用途**:延期支付(ATM/超商/AFTEE)取号后,展示虚拟帐号 / 缴费码 / 期限。即时支付(信用卡)也可用它查 `payStatus`
+**用途**:仅用于排查或兼容变更前已经创建的延期支付交易。新客户端统一调用 `/pay/omg/query` 查询信用卡/Apple Pay 状态,不再展示虚拟帐号或缴费码
 
 | 项 | 内容 |
 |----|------|
@@ -333,9 +334,9 @@ const poll = (ddId) => {
 
 > 轮询建议:每 2s 一次,最多 15 次(30s);超过仍未成功提示「支付结果确认中,请稍后在订单列表查看」。
 
-### 4.4 ATM/超商取号展示(延期支付
+### 4.4 历史 ATM/超商取号展示(不再开发
 
-用户选 ATM/超商后,付款不是即时的——OMG 先给一个虚拟帐号/缴费码,用户在期限内自己去缴。前端展示来自 §3.3 的 `info`
+以下示例只用于维护变更前交易,不属于新客户端范围
 
 ```html
 <view>银行代码:{{ info.BankCode }}</view>
@@ -368,7 +369,7 @@ const poll = (ddId) => {
 | `AFTEE_AFTEE` | AFTEE 先享后付 | 延期 | ❌ 人工 |
 
 > **即时支付**(信用卡/Apple Pay)→ 结果页轮询 `payStatus` 变 1 即成功。
-> **延期支付**(ATM/超商/AFTEE)→ 结果页展示取号信息,等用户线下缴款后 OMG 异步回调才置 1,无需前端轮询到成功
+> 新订单不再产生延期支付。历史 ATM/超商/AFTEE 交易仍由后端回调兼容,不应重新开放其前端入口
 
 ---
 
@@ -379,7 +380,7 @@ const poll = (ddId) => {
 | 配置项 | 含义 | 需要的值(示例) |
 |--------|------|----------------|
 | `omg.order-result-url` | 付款后跳回的前端结果页 | `https://app.xxx.com/#/pages/pay/omgResult` |
-| `omg.payment-info-url` | ATM/超商取号回调(后端地址,**前端不用管**) | `https://api.xxx.com/pay/omg/paymentInfo` |
+| 历史 `paymentInfo` 路由 | 仅兼容变更前 ATM/超商交易,**前端不用管** | 不进入新表单 |
 | `omg.return-url` | 服务端结果回调(后端地址,**前端不用管**) | `https://api.xxx.com/pay/omg/notify` |
 
 > 前端只需要确认:**`omgResult` 页面的完整 URL**(含 hash 路由或 history 路由),交给后端填 `omg.order-result-url`。测试期可用测试域名。
@@ -393,8 +394,8 @@ const poll = (ddId) => {
 3. **iOS web-view 不可另开窗**——收银台必须是完整页 web-view,不能 `window.open`、不能 iframe 嵌套。
 4. **query 传参必须 encodeURIComponent**——`MerchantTradeDate` 含 `/` 和空格,不编码会截断。
 5. **多次发起**——同一订单可重复发起(每次新 `MerchantTradeNo`),后端按 OMG 交易号 `tradeNo` 幂等,前端重复点「支付」不会重复扣款,但仍建议按钮防抖(后端已加 1s `@RepeatSubmit`)。
-6. **延期支付别轮询到成功**——ATM/超商是线下缴款,可能几小时甚至跨天,前端展示取号信息即可,不要无限轮询
-7. **退款只对信用卡/Apple Pay 自动**——ATM/超商选了就退不了,接口会返回「需人工」,前端按提示展示
+6. **不要重新开放延期支付入口**——新表单只认 `ChoosePayment=Credit`、`UnionPay=2`;历史回调兼容不代表可创建 ATM/超商交易
+7. **退款范围**——新订单只有信用卡/Apple Pay;历史 ATM/超商交易若出现退款仍按后台人工流程处理
 
 ---
 

+ 1 - 1
specs/016-omg-payment/payment-attempt-lifecycle.md

@@ -79,7 +79,7 @@ OMG 后台截图证明付的正是 **id=26(最新那条)**,不是较早的
 ## 3. 硬约束
 
 1. **MerchantTradeNo 提交到收银台后永久绑定、不可复用**(OMG 官方明文)。
-2. **延期支付有效期长**:`ChoosePayment=ALL`,用户可选 ATM/超商/BarcodeATM/AFTEE,交易有效期到 `ExpireDate`(ATM 默认 3 天、最长 60 天;超商 3 天;条码 7 天;AFTEE 可达 21+ 天),期间随时可付。取号信息经 `/pay/omg/paymentInfo` 回调落 `callbackRaw`
+2. **历史延期支付兼容**:2026-08-13 前创建的 ATM/超商/BarcodeATM/AFTEE 交易仍可能在有效期内付款,取号与回调继续按原 `MerchantTradeNo` 归属;新订单已固定 `ChoosePayment=Credit`,不再产生延期支付入口
 3. **notify 可能丢/迟到**(stage 尤甚),`queryTrade` 是兜底;但补单现依赖"最新一条=被付的那条"假设,多 MTN 时失效。
 4. **换号有丢钱风险**:旧 MTN 实付但 notify 未到 → 把行 UPDATE 成新 MTN → 旧 MTN 迟到 notify `getByMerchantTradeNo` 查不到 → 钱付了订单永未支付。安全换号必须先 `queryTrade` 确认旧 MTN 未付;而 `queryTrade` 当前不可靠(问题 B)→ **问题 B 必须先修,换号才有安全网**。
 5. **模块边界**:外部 HTTP 工具/锁在 `ruoyi-admin`;实体/Mapper/XML 在 `ruoyi-system`,不可反向依赖。

+ 7 - 1
specs/016-omg-payment/plan.md

@@ -6,7 +6,7 @@
 
 ## Summary
 
-接入台湾 OMG(歐買尬,金流引擎 FunPoint)AIO 幕前支付作为餐饮订单的在线支付,替代蓝新 NewebPay(不再启用)。OMG 用 `CheckMacValue`(SHA256) 单字段签名(无 AES、无解密),支持全部支付方式(信用卡/Apple Pay/ATM/超商/BarcodeATM/AFTEE,`ChoosePayment=ALL`),门店级凭证,本期含信用卡退款。**OMG 全部代码独立新建,不复用任何蓝新金流代码/表**;仅复用平台共享基础设施(订单状态机、推送、订单日志)。
+接入台湾 OMG(歐買尬,金流引擎 FunPoint)AIO 幕前支付作为餐饮订单的在线支付,替代蓝新 NewebPay(不再启用)。OMG 用 `CheckMacValue`(SHA256) 单字段签名(无 AES、无解密),新支付订单仅提供信用卡和 Apple Pay(`ChoosePayment=Credit`、`UnionPay=2`),门店级凭证,本期含信用卡退款。**OMG 全部代码独立新建,不复用任何蓝新金流代码/表**;仅复用平台共享基础设施(订单状态机、推送、订单日志)。
 
 技术决策与备选见 [research.md](research.md),表结构见 [data-model.md](data-model.md),接口契约见 [contracts/api.md](contracts/api.md),验证场景见 [quickstart.md](quickstart.md)。
 
@@ -116,6 +116,12 @@ application.yml (+application-dev.yml) # 新增 omg.* 配置段
 - HTML ????????? HTTPS Universal Link/App Link?????? URL ? HTML ??????????????App ????? `/pay/omg/query` ?????
 - `OMG_APP_RETURN_URL` ?????????????????????????????? App scheme ?????
 
+## 2026-08-13 支付方式范围重大调整
+
+- 新支付订单仅允许信用卡和 Apple Pay。OMG 官方将 Apple Pay 包含在 `Credit` 下,因此固定传 `ChoosePayment=Credit`;同时传 `UnionPay=2` 隐藏银联。
+- 新表单移除 `ExpireDate`、`StoreExpireDate`、`BarcodeATMExpireDate`、`ClientRedirectURL`,所有保留字段(含 `UnionPay`)统一参加 CheckMacValue 计算。
+- ATM、CVS、BarcodeATM、AFTEE 不再向新订单开放。已存在的历史回调解析与路由暂时保留,避免变更前交易无法归属。
+
 ## Complexity Tracking
 
 > Constitution Check 无违规,无需填表。

+ 6 - 6
specs/016-omg-payment/quickstart.md

@@ -48,12 +48,12 @@
 
 ---
 
-## 场景 D:非即时支付 ATM/超商(US3
+## 场景 D:支付方式范围(P0
 
-1. `ChoosePayment=ALL` 时消费者在 OMG 选 ATM 或超商
-2. OMG 回调 `/pay/omg/paymentInfo` 带虚帐/缴费码 → 平台展示(前端页)与期限
-3. 消费者期限内付款 → OMG 回调 `/pay/omg/notify`(`RtnCode=1`)→ 同场景 B 核销
-4. **预期**:`pay_type=ATM/CVS`,超期未付订单可重试或失效
+1. 创建新 OMG 支付表单
+2. 核对 `ChoosePayment=Credit`、`UnionPay=2`,且两字段都已参加 CheckMacValue
+3. 核对不存在 `ClientRedirectURL`、`ExpireDate`、`StoreExpireDate`、`BarcodeATMExpireDate`
+4. 打开 stage 收银台,确认只显示信用卡与 Apple Pay,不显示银联、ATM、CVS、BarcodeATM 或 AFTEE
 
 ---
 
@@ -68,5 +68,5 @@
 
 ## 完成判据
 
-- 场景 A/B 全绿;C 的伪造/重复/金额不符/模拟均被正确拦截;D 的 ATM/超商能拿到付款信息并核销;E 的信用卡退款状态与资金同步。
+- 场景 A/B 全绿;C 的伪造/重复/金额不符/模拟均被正确拦截;D 的收银台只显示信用卡与 Apple Pay;E 的信用卡退款状态与资金同步。
 - 全程 OMG 代码无任何 `newebpay` 依赖(grep 验证)。

+ 1 - 1
specs/016-omg-payment/research.md

@@ -128,7 +128,7 @@ stage `payment-stage.funpoint.com.tw` + 测试商店凭证/测试卡(文档附
 
 ## D13. 代码表与枚举(reference 02–05,2026-07-29 补读)
 
-- **ChoosePayment**(请求):本期 `ALL`;细分 Credit(信用卡/銀聯/Apple Pay)/ATM(FIRST/CHINATRUST/UBOT/KGI)/CVS(CVS/FAMILY/IBON/HILIFE)/BarcodeATM(CHINATRUST)/AFTEE
+- **ChoosePayment**(请求,2026-08-13 调整):新订单固定 `Credit` 并传 `UnionPay=2`,只显示信用卡与 Apple Pay;其余方式仅保留历史回调兼容
 - **Apple Pay 归在 Credit 下**(非独立 ChoosePayment);回覆 PaymentType 为 `Credit_CreditCard`,**仅凭 PaymentType 无法区分信用卡与 Apple Pay**(本期不区分)。
 - **回覆 PaymentType**(回调,落 `pos_order_omg_payment.pay_type`):`Credit_CreditCard`、`BarcodeATM_CHINATRUST`(即时);`ATM_FIRST/CHINATRUST/UBOT/KGI`、`CVS_CVS/FAMILY/IBON/HILIFE`、`AFTEE_AFTEE`(延期,走 PaymentInfoURL 取号 → US3)。
 - **RtnCode**:`1`=成功,其余失败;完整表为**图片**且持续新增,须查「歐買尬廠商後台→系統開發管理→交易狀態代碼查詢」。实现:`==1` 成功核销/退款成功,非 1 失败并记 `rtn_code`+`rtn_msg`,**不硬编码错误码分支**。

+ 6 - 6
specs/016-omg-payment/spec.md

@@ -17,10 +17,10 @@
   - 生产:`https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5`
   - `POST`,`application/x-www-form-urlencoded`
 - **签名模型(关键差异)**:OMG 用 **`CheckMacValue`(SHA256,`EncryptType=1`)** 单字段签名;这与蓝新 NewebPay 的 `TradeInfo`(AES) + `TradeSha`(SHA256) 双层模型**不同**。结论:**需独立的 OMG 签名工具,不能复用 `NewebPayEncryptUtil`**。具体算法(参数排序、URI 编码、HashKey/HashIV 包夹、SHA256 大写)在文档附錄 `07_appendices` / 范例 `06_check-value-sample`,留待 plan/research 落地。
-- **支持的支付方式**(`ChoosePayment`):`Credit`(信用卡)、`ApplePay`、`ATM`、`CVS`(超商代码)、`BarcodeATM`、`AFTEE`、`ALL`。
-  - ⚠️ 文档**未把「LINE Pay」列为独立 ChoosePayment** —— 用户提及 LINE Pay,需确认其接入方式(可能走 Credit 子支付或独立通道),见 D4
+- **本项目启用的支付方式**:仅信用卡和 Apple Pay。OMG 官方将 Apple Pay 包含在 `ChoosePayment=Credit` 内;同时传 `UnionPay=2` 隐藏银联,不使用 `ALL`。
+  - ATM、CVS、BarcodeATM、AFTEE 和 LINE Pay 均不属于新支付订单范围
 - **幕前流程**:商户组参 + 计算 `CheckMacValue` → POST 到 `AioCheckOut/V5` → 消费者在 OMG 收银台支付 → OMG **服务端 POST 回 `ReturnURL`**(商户须回 `1|OK`)→ 可选 `OrderResultURL` 客户端跳转(仅即时支付方式支持)。
-- **非即时支付(ATM/超商/BarcodeATM)**:OMG 生成虚帐/缴费码 → 回调 `PaymentInfoURL`(服务端 POST)+ 可选 `ClientRedirectURL`(客户端展示)
+- **历史非即时支付兼容**:既有 ATM/超商回调入口保留用于接收变更前交易的迟到通知;新支付订单不再生成相关参数或入口
 - **退款**:`05_refund` API;**订单查询**:`04_order_query`;**付款结果通知**:`03_payment_notify`。
 - **发票**:`InvoiceMark` 必须 `N` —— 电子发票仍走本项目 ezPay 链路(010/014),与支付解耦。
 - **约束**:不可用 iframe;iOS 下不可另开新窗口;`MerchantTradeNo` 全平台唯一;`ReturnURL` 与 `OrderResultURL` 不可相同。
@@ -36,7 +36,7 @@
 
 ## 已定决策(D1–D6,2026-07-29 确认)
 
-- **D1 — 支付方式范围**:支持 OMG 全部支付方式(`ChoosePayment=ALL`:信用卡 Credit / Apple Pay / ATM / 超商 CVS / BarcodeATM / AFTEE)
+- **D1 — 支付方式范围(2026-08-13 调整)**:仅信用卡和 Apple Pay;新订单固定 `ChoosePayment=Credit`、`UnionPay=2`,不开放 ATM、CVS、BarcodeATM、AFTEE 或银联
 - **D2 — 与 NewebPay(011) 的关系**:**OMG 替代蓝新金流**,作为订单在线支付启用;NewebPay 不再启用。
 - **D3 — 退款/取消**:**本期范围**,订单取消走 OMG 退款 API(`05_refund`)。
 - **D4 — LINE Pay**:**本期不做**(文档未列为独立 ChoosePayment,后续再议)。
@@ -143,11 +143,11 @@ OMG 服务端回调(`ReturnURL`)到达后,验签 + 幂等更新订单,
 ### Functional Requirements
 
 - **FR-001**:系统 MUST 支持消费者对未支付订单发起 OMG 幕前在线支付,跳转 OMG 收银台(金额、订单号、商品描述正确)。
-- **FR-002**:系统 MUST 支持 OMG 全部支付方式(`ChoosePayment=ALL`:信用卡 / Apple Pay / ATM / 超商 / BarcodeATM / AFTEE);LINE Pay 本期不做(D4)
+- **FR-002**:系统 MUST 只向新支付订单提供信用卡和 Apple Pay,固定传 `ChoosePayment=Credit` 与 `UnionPay=2`;不得传 `ALL`,不得展示 ATM、CVS、BarcodeATM、AFTEE、银联或 LINE Pay
 - **FR-003**:系统 MUST 使用 OMG `CheckMacValue`(SHA256,`EncryptType=1`)对请求签名、对回调验签;签名工具**独立于**现有 NewebPay 工具。
 - **FR-004**:系统 MUST 在 OMG `ReturnURL` 回调时验签 + 按 `MerchantTradeNo` 幂等更新订单为已支付,并回 `1|OK`。
 - **FR-005**:系统 MUST 在支付成功后触发现有订单履约链路(状态流转 + iOS 推送 + `sendAcceptRiderPush`)。
-- **FR-006**:系统 MUST 支持非即时支付(ATM/超商)的虚帐/缴费码生成、展示与期限管理(`PaymentInfoURL` 回调)
+- **FR-006**:系统 MUST NOT 为新支付订单生成 ATM/超商取号或延期缴费参数;历史回调入口仅为变更前交易保留
 - **FR-007**:系统 MUST 支持订单取消时经 OMG 退款 API(`05_refund`)退款并同步状态(本期范围,D3)。
 - **FR-008**:系统 MUST 支持门店级 OMG 凭证配置(MerchantID/HashKey/HashIV,启用开关,测试/生产切换)。
 - **FR-009**:本期功能覆盖范围**仅餐饮订单**的在线支付;旅游·机票订单(015)后续衔接,不在本期(D5)。

+ 12 - 1
specs/016-omg-payment/tasks.md

@@ -53,7 +53,7 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 ## Phase 3: User Story 1 - 发起 OMG 支付 (Priority: P1) 🎯 MVP part 1
 
-**Goal**: C 端用户下单后调发起接口拿到 form 参数,前端 Form Post 跳转 OMG 收银台(ChoosePayment=ALL
+**Goal**: C 端用户下单后调发起接口拿到 form 参数,前端 Form Post 跳转 OMG 收银台(新订单仅信用卡与 Apple Pay
 
 **Independent Test**: 对已开通 OMG 的门店下单 → 调 `/pay/omg/create` → 返回 `gatewayUrl` + 全参数(含 CheckMacValue) → 前端跳转 OMG 页出现支付方式;`pos_order_omg_payment` 落一行(pay_status=0)、`pos_order.pay_type="7"`
 
@@ -223,6 +223,17 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 ---
 
+## Phase 15: 支付方式收敛为信用卡与 Apple Pay(Priority: P0)
+
+- [x] T088 将新支付表单固定为 `ChoosePayment=Credit`、`UnionPay=2`,并保证两个字段都参加 CheckMacValue 计算。
+- [x] T089 从新支付表单移除 `ExpireDate`、`StoreExpireDate`、`BarcodeATMExpireDate`、`ClientRedirectURL` 及对应无用配置;保留历史延期支付回调路由处理变更前交易。
+- [x] T090 更新表单契约测试源码,精确断言只生成信用卡/Apple Pay 所需字段且不生成延期支付字段。
+- [ ] T091 全部 OMG 功能调整结束后统一运行 Maven 编译、定向测试和 stage 收银台验证;本批按用户要求暂不执行。
+
+**Checkpoint**:代码层已限制新订单只进入信用卡(含 Apple Pay)收银台,并隐藏银联;历史延期交易兼容入口未删除。
+
+---
+
 ## Dependencies & Execution Order
 
 ### Phase 依赖