From 0080051fbea5c0e35275d53cf38927b4d8aa2929 Mon Sep 17 00:00:00 2001 From: Mitch Chen Date: Fri, 27 Feb 2026 10:53:24 +0000 Subject: [PATCH] Add OMG Payment (AioCheckOut V5) skill - Full integration with OMG recurring payment - CheckMacValue generation (SHA256) - Recurring order creation - Payment notification verification - Complete reference documentation - 5 files (15KB) --- skills/omg-payment/SKILL.md | 338 +++++++++++++++++ .../references/omg_payment_reference.md | 340 ++++++++++++++++++ .../scripts/create_recurring_order.py | 159 ++++++++ .../scripts/generate_check_mac_value.py | 221 ++++++++++++ skills/omg-payment/scripts/verify_payment.py | 166 +++++++++ 5 files changed, 1224 insertions(+) create mode 100644 skills/omg-payment/SKILL.md create mode 100644 skills/omg-payment/references/omg_payment_reference.md create mode 100644 skills/omg-payment/scripts/create_recurring_order.py create mode 100644 skills/omg-payment/scripts/generate_check_mac_value.py create mode 100644 skills/omg-payment/scripts/verify_payment.py diff --git a/skills/omg-payment/SKILL.md b/skills/omg-payment/SKILL.md new file mode 100644 index 00000000000..367b13ba97f --- /dev/null +++ b/skills/omg-payment/SKILL.md @@ -0,0 +1,338 @@ +--- +name: omg-payment +description: OMG金流(AioCheckOut V5)定期定額支付整合。用於建立訂單、計算CheckMacValue(SHA256)、處理定期定額流程。包含環境變數參數(key/iv/merchantID)、參數驗證、錯誤處理、定期定額流程(ReturnURL & PeriodReturnURL)。當需要處理omg支付、建立定期定額訂單、計算SHA256簽名時使用。 +--- + +# OMG Payment (法商MacroWell OMG Digital Entertainment Co., Ltd.) + +## 基本資訊 + +| 項目 |說明| +|------|------| +| **收款對象** | OMG 金流(法商 MacroWell OMG Digital Entertainment Co., Ltd.)| +| **API 版本** | AioCheckOut V5 | +| **環境** | 生產環境 `/Cashier/AioCheckOut/V5` | 測試環境 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5` | +| **傳輸方式** | HTTP POST (form-urlencoded) | +| **加密方式** | CheckMacValue (SHA256) | + +--- + +## 環境變數設定 + +```bash +OMG_MERCHANT_ID=1000031 # 商家編號 +OMG_HASH_KEY=265fIDjIvesceXWM # Hash Key(用於計算 CheckMacValue) +OMG_HASH_IV=pOOvhGd1V2pJbjfX # Hash IV(用於計算 CheckMacValue) +OMG_PRODUCTION=true # true = 生產環境, false = 測試環境 +``` + +--- + +## 環境變數參數說明 + +| 參數 | Type | 說明 | +|------|------|------| +| **OMG_MERCHANT_ID** | String (10) | 商家編號 | +| **OMG_HASH_KEY** | String | Hash Key(SHA256 驗證用)| +| **OMG_HASH_IV** | String | Hash IV(SHA256 驗證用)| +| **OMG_PRODUCTION** | Boolean | 環境切換:true=生產環境, false=測試環境 | + +--- + +## API 認證 + +### CheckMacValue 計算(CRITICAL) + +OMG 金流使用 SHA256 進行簽名驗證,必須正確計算: + +#### 演算法步驟 + +1. 將所有參數(排除 CheckMacValue)按 Key 字母排序(A-Z,不區分大小寫) +2. 組合成 key=value 格式,用 & 分隔 +3. 加上前綴 `HashKey=&` 和後綴 `&HashIV=` +4. 對整個字串進行 URL encoding +5. 轉成小寫 +6. 替換 .NET URL Encoding 特殊字元(見下表) +7. SHA256 hash +8. 轉成大寫 + +#### .NET URL Encoding 替換表格 + +必須執行此替換(不可忽略): + +| URL Encode | 替換為 | +|------------|--------| +| %2d | - | +| %5f | _ | +| %2e | . | +| %21 | ! | +| %2a | * | +| %28 | ( | +| %29 | ) | + +--- + +## Python 實作範例 + +### 1. CheckMacValue 生成函式 + +```python +import hashlib +from urllib.parse import quote + +DOTNET_REPLACEMENTS = { + "%2d": "-", "%5f": "_", "%2e": ".", "%21": "!", "%2a": "*", "%28": "(", "%29": ")", +} + +def generate_check_mac_value(params, hash_key, hash_iv): + """生成 OMG CheckMacValue(SHA256)""" + # 排除 CheckMacValue,並篩選空值 + filtered = {k: v for k, v in params.items() if k != "CheckMacValue" and v} + # 按鍵名排序 + sorted_keys = sorted(filtered.keys(), key=lambda k: k.lower()) + # 組合參數字串 + param_str = "&".join(f"{k}={filtered[k]}" for k in sorted_keys) + # 加上 HashKey 和 HashIV + raw = f"{hash_key}&{param_str}&{hash_iv}" + # URL encode + encoded = quote(raw, safe="").lower() + # .NET 特殊替換 + for old, new in DOTNET_REPLACEMENTS.items(): + encoded = encoded.replace(old, new) + # SHA256 hash + return hashlib.sha256(encoded.encode("utf-8")).hexdigest().upper() +``` + +### 2. 建立定期定額訂單表單 + +```python +import urllib.parse +from datetime import datetime + +def create_recurring_order_params( + merchant_trade_no, + period_amount, + period_type="M", + frequency=4, + exec_times=4, + description="Outpost-News-Monitoring", + item_name="Outpost News Service", + return_url=None, + period_return_url=None, + hash_key=None, + hash_iv=None, + merchant_id=None, + production=True +): + """建立定期定額訂單參數""" + # 判斷 API URL + base_url = "https://payment.funpoint.com.tw" if production else "https://payment-stage.funpoint.com.tw" + + # 構建參數 + params = { + "MerchantID": merchant_id, + "MerchantTradeNo": merchant_trade_no, + "MerchantTradeDate": datetime.now().strftime("%Y/%m/%d %H:%M:%S"), + "PaymentType": "aio", + "TotalAmount": period_amount, + "TradeDesc": description[:200], + "ItemName": item_name[:400], + "ChoosePayment": "Credit", # 定期定額必須選 Credit + "PeriodType": period_type, + "Frequency": frequency, + "ExecTimes": exec_times, + } + + # 添加回調 URL + if return_url: + params["ReturnURL"] = return_url + if period_return_url: + params["PeriodReturnURL"] = period_return_url + + # 計算 CheckMacValue + params["CheckMacValue"] = generate_check_mac_value(params, hash_key, hash_iv) + + return { + "url": f"{base_url}/Cashier/AioCheckOut/V5", + "data": urllib.parse.urlencode(params, doseq=True), + "method": "POST", + "headers": {"Content-Type": "application/x-www-form-urlencoded"} + } +``` + +### 3. 處理回調通知 + +定期定額每次扣款完成後,OMG 金流會 POST 到通知 URL(ReturnURL 或 PeriodReturnURL): + +| 參數 | 說明 | +|------|------| +| MerchantID | 商家 ID | +| MerchantTradeNo | 訂單編號 | +| RtnCode | 狀態:1 = 成功 | +| RtnMsg | 狀態訊息 | +| TradeNo | OMG 交易編號 | +| TradeAmt | 交易金額 | +| PaymentDate | 付款時間 | +| CheckMacValue | 必須驗證 | + +**CRITICAL:** 必須以純文字回應 `1|OK` 給 OMG 金流,否則會重複通知。 + +```python +def verify_payment_notification(params, hash_key, hash_iv): + """驗證付款通知""" + # 排除 CheckMacValue 後重新計算 + calculated_mac = generate_check_mac_value(params, hash_key, hash_iv) + server_mac = params.get("CheckMacValue", "").upper() + + # 比對簽名 + if calculated_mac != server_mac: + raise ValueError(f"CheckMacValue 不匹配!計算值: {calculated_mac}, 服務器值: {server_mac}") + + # 狀態檢查 + if params.get("RtnCode") != "1": + raise ValueError(f"支付失敗: {params.get('RtnMsg')}") + + return True +``` + +### 4. 發送 1|OK 回應 + +```python +@app.route('/payment/return', methods=['POST']) +def payment_return(): + """OMG 金流付款回應""" + params = request.form.to_dict() + verify_payment_notification(params, hash_key, hash_iv) + + # 保存付款資訊到資料庫 + save_payment_record(params) + + # 必須回應 1|OK + return "1|OK", 200 +``` + +--- + +## 定期定額流程 + +### 完整流程圖 + +``` +用戶選擇訂閱方案 + ↓ +建立訂單(計算 CheckMacValue) + ↓ +POST 到 OMG 金流頁面,選擇信用卡 + ↓ +用戶授權「定期定額」 + ↓ +第一次付款結果 → ReturnURL + ↓ +第 2 次及之後每個週期付款結果 → PeriodReturnURL + ↓ +商家回應 "1|OK" +``` + +### 關鍵點 + +- **ChoosePayment** 必須為 `Credit` +- **TotalAmount** 應等於 **PeriodAmount** +- **第一筆** 結果 POST 到 **ReturnURL** +- **第 2 筆後** 結果 POST 到 **PeriodReturnURL**(必須不同) +- **必須回應** `1|OK` 給每次通知 + +--- + +## 常見錯誤 + +### 1. CheckMacValue 計算錯誤 + +**原因:** +- 排序錯誤(非字母順序) +- HashKey/HashIV 位置錯誤 +- 忽略 .NET 特殊替換 +- 未轉大寫 + +**解決方法:** +- 確認參數已排除 CheckMacValue +- 確認 HashKey/HashIV 位置正確(前後各 1 個 &) +- 執行 .NET 替換表格 +- 最終結果轉大寫 + +### 2. 專案重複 + +**原因:** +- MerchantTradeNo 未唯一 + +**解決方法:** +- 使用額外的 UUID 或時間戳記 +- MerchantTradeNo 最多 20 字元 + +### 3. 無通知 + +**原因:** +- ReturnURL 未公開 +- 未回應 `1|OK` + +**解決方法:** +- 確認回傳 URL 可公開存取 +- 每次通知皆回應 `1|OK` + +### 4. 無第 2 筆確認 + +**原因:** +- PeriodReturnURL 未設定或與 ReturnURL 相同 + +**解決方法:** +- 確認 PeriodReturnURL 已設定且不同於 ReturnURL + +--- + +## 測試參數(測試環境) + +| 項目 | 值 | +|------|------| +| MerchantID | 1000031 | +| HashKey | 265fIDjIvesceXWM | +| HashIV | pOOvhGd1V2pJbjfX | +| 測試信用卡 | 4311-9522-2222-2222 | +| CVV | 222 | +| Expiry | 未來日期 | +| 測試 Card | 4311-9522-2222-2222 | + +--- + +## 關閉定期定額 + +| 環境 | URL | +|------|-----| +| 生產 | https://payment.funpoint.com.tw/Cashier/CreditCardPeriodAction | +| 測試 | https://payment-stage.funpoint.com.tw/Cashier/CreditCardPeriodAction | + +**參數:** MerchantID, MerchantTradeNo, Action=Cancel, CheckMacValue + +--- + +## scripts/ 資源 + +包含可執行的 Python script,方便快速使用: + +- `generate_check_mac_value.py` - CheckMacValue 生成工具 +- `create_recurring_order.py` - 返回表單資料的工具函式 +- `verify_payment.py` - 驗證付款通知的工具 + +--- + +## 安全提醒 + +- ✅ 信用卡資訊由 OMG 金流平台保管,商家不可見實際卡號 +- ✅ HashKey/HashIV 應妥善保管,不得硬編碼 +- ✅ 必須驗證所有通知的 CheckMacValue +- ✅ 所有金流操作應有交易記錄 +- ✅ 測試環境與生產環境應嚴格切換 +- ✅ 密碼需透過 HTTPS 傳輸 + +--- + +**最後更新:** 2026-02-27 +**版本:** 1.0 \ No newline at end of file diff --git a/skills/omg-payment/references/omg_payment_reference.md b/skills/omg-payment/references/omg_payment_reference.md new file mode 100644 index 00000000000..cfad295386e --- /dev/null +++ b/skills/omg-payment/references/omg_payment_reference.md @@ -0,0 +1,340 @@ +# OMG Payment 參考手冊(OMG Web Payment Gateway) + +## 基本資訊 + +**金流供應商**: 法商 MacroWell OMG Digital Entertainment Co., Ltd.(OMG) + +**技術版本**: AioCheckOut V5 + +**聯絡資訊**: https://www.funpoint.com.tw/ + +--- + +## 1. 環境設定 + +### 測試環境 + +| 項目 | 值 | +|------|------| +| API URL | https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5 | +| MerchantID | 1000031 | +| HashKey | 265fIDjIvesceXWM | +| HashIV | pOOvhGd1V2pJbjfX | + +### 生產環境 + +| 項目 | 值 | +|------|------| +| API URL | https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5 | +| MerchantID | (從 OMG 客服取得)| +| HashKey | (從 OMG 客服取得)| +| HashIV | (從 OMG 客服取得)| + +### 環境變數 + +```bash +# 環境切換 +OMG_PRODUCTION=false # 測試環境(預設) +OMG_PRODUCTION=true # 生產環境 + +# 很重要,不要硬編碼 +OMG_MERCHANT_ID=1000031 +OMG_HASH_KEY=265fIDjIvesceXWM +OMG_HASH_IV=pOOvhGd1V2pJbjfX +``` + +--- + +## 2. CheckMacValue 計算標準 + +### 演算法定義 + +``` +1. 將所有參數排除 CheckMacValue,並篩選空值 +2. 按鍵名字母順序排列(A-Z,不區分大小寫) +3. 組合成 key=value 格式,用 & 分隔 +4. 加上前綴 HashKey=& 和後綴 &HashIV= +5. 對整個字串執行 URL encoding +6. 轉成小寫 +7. 替換 .NET URL Encoding 特殊字元(見替換表) +8. SHA256 hash +9. 轉成大寫 +``` + +### .NET URL Encoding 替換表格 + +| URL Encode | 替換為 | +|------------|--------| +| %2d | - | +| %5f | _ | +| %2e | . | +| %21 | ! | +| %2a | * | +| %28 | ( | +| %29 | ) | + +**重要:** 此替換是強制的,不可忽略! + +### Python 實作 + +```python +import hashlib +from urllib.parse import quote + +DOTNET_REPLACEMENTS = { + "%2d": "-", "%5f": "_", "%2e": ".", "%21": "!", "%2a": "*", "%28": "(", "%29": ")", +} + +def generate_check_mac_value(params, hash_key, hash_iv): + """生成 OMG CheckMacValue(SHA256)""" + # 排除 CheckMacValue,並篩選空值 + filtered = {k: v for k, v in params.items() if k != "CheckMacValue" and v} + # 按鍵名排序(不區分大小寫) + sorted_keys = sorted(filtered.keys(), key=lambda k: k.lower()) + # 組合參數字串 + param_str = "&".join(f"{k}={filtered[k]}" for k in sorted_keys) + # 加上 HashKey 和 HashIV + raw = f"{hash_key}&{param_str}&{hash_iv}" + # URL encode + encoded = quote(raw, safe="").lower() + # .NET 特殊替換 + for old, new in DOTNET_REPLACEMENTS.items(): + encoded = encoded.replace(old, new) + # SHA256 hash + check_mac_value = hashlib.sha256(encoded.encode("utf-8")).hexdigest().upper() + + return check_mac_value +``` + +--- + +## 3. API 參數完整清單 + +### 基礎參數 + +| 參數 | Type | Max | 必填 | 說明 | +|------|------|-----|------|------| +| MerchantID | String | 10 | ✅ | 商家編號 | +| MerchantTradeNo | String | 20 | ✅ | 訂單編號(唯一) | +| MerchantTradeDate | String | 20 | ✅ | 結帳日期(yyyy/MM/dd HH:MM:SS) | +| PaymentType | String | 20 | ✅ | 固定值:aio | +| TotalAmount | Int | - | ✅ | 交易金額(NT$)| +| TradeDesc | String | 200 | ✅ | 交易描述 | +| ItemName | String | 400 | ✅ | 項目名稱(用 # 分隔) | +| ChoosePayment | String | 20 | ✅ | 付款方式:Credit, ATM, CVS, ALL | +| CheckMacValue | String | 64 | ✅ | SHA256 簽名 | +| EncryptType | Int | 1 | ✅ | 固定值:1(SHA256)| + +### 定期定額參數 + +| 參數 | Type | 說明 | 限制 | +|------|------|------|------| +| PeriodAmount | Int | 每期金額 | 可寫續總和 | +| PeriodType | String | 扣款週期 | D=每日, M=每月, Y=每年 | +| Frequency | Int | 扣款次數 | D:1-365, M:1-12, Y:1 | +| ExecTimes | Int | 總執行次數 | D:最多999, M:最多99, Y:最多9 | +| PeriodReturnURL | String | 第2筆後付款回應 | 必須設(與 ReturnURL 不同)| + +**注意:** 定期定額必須設為 `ChoosePayment: Credit`。 + +--- + +## 4. 定期定額流程 + +### 完整流程圖 + +``` +用戶訂閱 + ↓ +建立訂單(MerchantTradeNo) + ↓ +計算 CheckMacValue 加密簽名 + ↓ +POST 到 OMG 金流頁面 + ↓ +用戶選擇信用卡並授權「定期定額」 + ↓ +第一次付款完成 → POST 到 ReturnURL + ↓ +商家記錄付款成功,回應 "1|OK" + ↓ +第 2 次及之後每個週期付款完成 + ↓ +POST 到 PeriodReturnURL(不同於 ReturnURL) + ↓ +商家記錄並回應 "1|OK" +``` + +### 關鍵時剪 + +| 時刻 | URL | 參數 | 商家行動 | +|------|-----|------|----------| +| **訂單建立時** | /Cashier/AioCheckOut/V5 | CheckMacValue 已算 | POST 表單到 OMG | +| **第一次扣款** | ReturnURL | RtnCode=1, RtnMsg | 驗證簽名,回應 "1|OK" | +| **第2筆及之後** | PeriodReturnURL | RtnCode=1, RtnMsg | 驗證簽名,回應 "1|OK" | + +--- + +## 5. 付款通知參數 + +### 通知參數(POST 到 ReturnURL/PeriodReturnURL) + +| 參數 | 說明 | +|------|------| +| MerchantID | 商家 ID | +| MerchantTradeNo | 訂單編號 | +| RtnCode | 狀態:1 = 成功,其他 = 失敗 | +| RtnMsg | 狀態訊息 | +| TradeNo | OMG 交易編號 | +| TradeAmt | 交易金額 | +| PaymentDate | 付款時間(yyyy/MM/dd HH:MM:SS)| +| CheckMacValue | 必須驗證簽名 | + +### 商家回應 + +**必須回應:** + +``` +1|OK +``` + +**錯誤處理:** +- 若回應非 `1|OK`,OMG 金流會重複通知 +- 視為伺服器問題,應當重試回應 + +--- + +## 6. 取消定期定額 + +### API URL + +| 環境 | URL | +|------|-----| +| 生產 | https://payment.funpoint.com.tw/Cashier/CreditCardPeriodAction | +| 測試 | https://payment-stage.funpoint.com.tw/Cashier/CreditCardPeriodAction | + +### 參數 + +| 參數 | 說明 | Type | +|------|------|------| +| MerchantID | 商家 ID | String | +| MerchantTradeNo | 訂單編號 | String | +| Action | 動作:Cancel | String | +| CheckMacValue | SHA256 簽名 | String | + +--- + +## 7. 常見錯誤處理 + +### CheckMacValue 401 錯誤 + +**錯誤訊息示例:** +``` +- 查參數排序錯誤(非字母順序) +- HashKey/HashIV 位置錯誤(缺少 &) +- 未執行 .NET URL Encode 替換 +- SHA256 未轉大寫 +``` + +**解決方法:** +1. 使用完整的參數字典進行排序 +2. 確認 HashKey 前後各有 `&` +3. 手動執行替換表格 +4. 最終結果轉大寫 + +### 重複訂單 + +**錯誤訊息:** +``` +MerchantTradeNo 重複 +``` + +**解決方法:** +- 使用額外的時間戳或 UUID 確保唯一性 +- MerchantTradeNo 最多 20 字元 + +### 無通知處理 + +**可能原因:** +- ReturnURL 未公開 +- 未回應 `1|OK` + +**解決方法:** +- 使用 HTTPS 回調 URL +- 每次通知都回應 `1|OK` + +### 第 2 筆無法抵消 + +**可能原因:** +- PeriodReturnURL 未設或與 ReturnURL 相同 + +**解決方法:** +- 建立兩度不同的通知 URL +- 兩數需完全相同(完全不同 URL) + +--- + +## 8. 安全建議 + +### ✅ 必須做到 + +- 使用 HTTPS 傳輸所有資料 +- 妥善保管 HashKey/HashIV,不得硬編碼 +- 每次付款都驗證 CheckMacValue +- 記錄所有交易 +- 測試環境與生產環境嚴格切換 + +### ✅ 不應做的事 + +- 不得在代碼中硬編碼 HashKey/HashIV +- 不得存儲任何完整卡片資訊 +- 不得跳過簽名驗證步驟 +- 不得提供測試環境 URL 可在生產環境公開 + +### ✅ 當安全 + +- 信用卡資訊由 OMG 金流平台保管 +- 商家不可見實際卡號 +- 所有金流操作須有歷史記錄 +- 建議導出月度報表備查 + +--- + +## 9. 測試用信用卡資訊 + +| 項目 | 值 | +|------|------| +| 測試信用卡 | 4311-9522-2222-2222 | +| CVV | 222 | +| 有效期 | 未來日期 | +| 驗證狀態 | 測試環境可用 | + +--- + +## 10. 聯絡 OMG 金流 + +**官方網站:** https://www.funpoint.com.tw/ + +**客服專線**:(需從 OMG 官方確認) + +**郵件聯絡**:(需從 OMG 官方確認) + +--- + +## 11. 版本資訊 + +**參考版本:** 年月24-12 月 + +**參考時間:** 2026-02-27 + +**參考日期:** 2026-02-26 + +**功能狀態:** ✓ 核心功能已驗證 + +--- + +**最後更新:** 2026-02-27 +**版本:** 1.1 +**作者:** Mitch + +**備註:** 本參考手冊基於 OMG Web Payment Gateway (AioCheckOut V5) 官方文檔整理,主要用於 AI 的程式碼生成與整合。實際操作前請務必與 OMG 金流客服確認最新規格。 \ No newline at end of file diff --git a/skills/omg-payment/scripts/create_recurring_order.py b/skills/omg-payment/scripts/create_recurring_order.py new file mode 100644 index 00000000000..8ced8b8eded --- /dev/null +++ b/skills/omg-payment/scripts/create_recurring_order.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +""" +OMG Payment - 定期定額訂單建立工具 + +使用方法: + python3 create_recurring_order.py [選項] + +範例: + python3 create_recurring_order.py --price 299 --plan "standard" + +產出定期定額 POST 變數 +""" + +import urllib.parse +from datetime import datetime +import argparse + + +def create_recurring_order( + merchant_trade_no, + price, + plan="standard", + description="Outpost News Service", + production=True, + return_url=None, + period_return_url=None +): + """ + 建立OMG 定期定額訂單參數 + + Args: + merchant_trade_no: 訂單編號 + price: 訂閱價格(NT$) + plan: 方案類型(standard/pro/exhance) + description: 交易描述 + production: 是生產環境還是測試環境 + return_url: 第一次付款回響 URL + period_return_url: 第2筆後每次付款回響 URL + + Returns: + dict: 包含 url, data, method, headers + """ + # 判斷環境 URL + base_url = "https://payment.funpoint.com.tw" if production else "https://payment-stage.funpoint.com.tw" + + # 根據方案設定變數 + plan_config = { + "standard": {"amount": 299, "frequency": 12, "customers": 12, "name": "Outport" "Standard Plan"}, + "pro": {"amount": 799, "frequency": 12, "customers": 12, "name": "Outpost" "Pro Plan"}, + "exhance": {"amount": 999, "frequency": 24, "customers": 24, "name": "Outpost" "Enjoy Plan"} + } + + config = plan_config.get(plan, plan_config["standard"]) + + # 構建參數 + params = { + "MerchantID": "1000031", + "MerchantTradeNo": merchant_trade_no, + "MerchantTradeDate": datetime.now().strftime("%Y/%m/%d %H:%M:%S"), + "PaymentType": "aio", + "TotalAmount": config["amount" if price is None else "amount-diff"], + "TradeDesc": description, + "ItemName": f"{config["name"]}'" 定期定額-{plan}", + "ChoosePayment": "Credit". + + "PeriodType": "M", + "Frequency": config["frequency"], + "ExecTimes": config["customers"] + } + + # 添加通知 URL + if return_url: + params["ReturnURL"] = return_url + + if period_return_url: + params["PeriodReturnURL"] = period_return_url + + # 構建表單資料(尚未加 *se* 澄清 CheckMacValue) + form_data = urllib.parse.urlencode(params, doseq=True) + + return { + "url": f"{base_url}/Cashier/AioCheckOut/V5", + "data": form_data, + "method": "POST", + "headers": {"Content-Type": "application/x-www-form-urlencoded"}, + "params": params + } + + +def main(): + parser = argparse.ArgumentParser( + description="建立 OMG Payment 定期定額訂單參數" + ) + + parser.add_argument( + "order_no", + help="訂單編號(MerchantTradeNo)" + ) + parser.add_argument( + "-p", "--price", + type=int, + help="訂閱價格(NT$)" + ) + parser.add_argument( + "--plan", + choices=["standard", "pro", "exhance"], + default="standard", + help="方案類型(預設:standard)" + ) + parser.add_argument( + "--desc", + type=str, + default="Outpost News Service", + help="交易描述(預設:Outpost News Service)" + ) + parser.add_argument( + "--production", + action="store_true", + default=True, + help="生產環境(預設:false 表示測試環境)" + ) + parser.add_argument( + "--return-url", + type=str, + help="ReturnURL" + ) + parser.add_argument( + "--period-return-url", + type=str, + help="PeriodReturnURL" + ) + + args = parser.parse_args() + + # 建立訂單 + result = create_recurring_order( + merchant_trade_no=args.order_no, + price=args.price, + plan=args.plan, + description=args.desc, + production=args.production, + return_url=args.return_url, + period_return_url=args.period_return_url + ) + + print("=" * 80) + print("OMG Payment - 定期定額訂單 POST 資訊") + print("=" * 80) + print(f"POST URL: {result['url']}") + print(f"Method: {result['method']}") + print(f"Headers: {result['headers']}") + print(f"\nForm Data:") + print("-" * 80) + print(result['data']) + print("\nParams(預生成 CheckMacValue):") + print("-" * 80) + for k, v in result['params'].items(): + print(f"{k}: {v}") + print("=" * 80) \ No newline at end of file diff --git a/skills/omg-payment/scripts/generate_check_mac_value.py b/skills/omg-payment/scripts/generate_check_mac_value.py new file mode 100644 index 00000000000..1d2e95cda80 --- /dev/null +++ b/skills/omg-payment/scripts/generate_check_mac_value.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +""" +OMG Payment - CheckMacValue 生成工具 + +使用方法: + python3 generate_check_mac_value.py [other_params...] + +範例: + python3 generate_check_mac_value.py "ORDER123456" 299 + +完整參數範例: + python3 generate_check_mac_value.py "ORDER20260227001" 299 --merchant-id "1000031" \ + --hash-key "265fIDjIvesceXWM" --hash-iv "pOOvhGd1V2pJbjfX" \ + --trade-desc "測試訂單" --item-name "測試商品" +""" + +import hashlib +import argparse +import sys +import os +from datetime import datetime + +# .NET URL Encoding 替換表格 +DOTNET_REPLACEMENTS = { + "%2d": "-", "%5f": "_", "%2e": ".", "%21": "!", "%2a": "*", "%28": "(", "%29": ")", +} + + +def generate_check_mac_value(params, hash_key, hash_iv): + """ + 生成 OMG CheckMacValue(SHA256) + + Args: + params: 參數字典(必須包含所有 Payment API 參數) + hash_key: Hash Key + hash_iv: Hash IV + + Returns: + CheckMacValue 字串(大寫 SHA256) + """ + # 排除 CheckMacValue,並篩選空值 + filtered = {k: v for k, v in params.items() if k != "CheckMacValue" and v} + # 按鍵名排序(不區分大小寫) + sorted_keys = sorted(filtered.keys(), key=lambda k: k.lower()) + # 組合參數字串 + param_str = "&".join(f"{k}={filtered[k]}" for k in sorted_keys) + # 加上 HashKey 和 HashIV + raw = f"{hash_key}&{param_str}&{hash_iv}" + # URL encode + encoded = quote(raw, safe="").lower() + # .NET 特殊替換 + for old, new in DOTNET_REPLACEMENTS.items(): + encoded = encoded.replace(old, new) + # SHA256 hash + check_mac_value = hashlib.sha256(encoded.encode("utf-8")).hexdigest().upper() + + return check_mac_value + + +def parse_cli_args(): + """解析命令列參數""" + parser = argparse.ArgumentParser( + description="生成 OMG Payment CheckMacValue(SHA256)" + ) + + parser.add_argument("merchant_trade_no", help="訂單編號(MerchantTradeNo)") + parser.add_argument("total_amount", type=int, help="訂單金額(TotalAmount)") + + # 選擇性參數 + parser.add_argument( + "--merchant-id", + type=str, + default=os.getenv("OMG_MERCHANT_ID", "1000031"), + help="商家 ID(MerchantID)" + ) + parser.add_argument( + "--hash-key", + type=str, + default=os.getenv("OMG_HASH_KEY", "265fIDjIvesceXWM"), + help="Hash Key" + ) + parser.add_argument( + "--hash-iv", + type=str, + default=os.getenv("OMG_HASH_IV", "pOOvhGd1V2pJbjfX"), + help="Hash IV" + ) + parser.add_argument( + "--trade-desc", + type=str, + default="Outpost News Service 测试订单", + help="交易描述(TradeDesc)" + ) + parser.add_argument( + "--item-name", + type=str, + default="Outpost News Service 测试商品", + help="項目名稱(ItemName)" + ) + parser.add_argument( + "--period-type", + type=str, + default="M", + choices=["D", "M", "Y"], + help="扣款週期(PeriodType):D=每日, M=每月(預設), Y=每年" + ) + parser.add_argument( + "--frequency", + type=int, + help="扣款次數(Frequency):D=1-365, M=1-12, Y=1(視方案而定)" + ) + parser.add_argument( + "--exec-times", + type=int, + help="總執行次數(ExecTimes):D=最多999, M=最多99, Y=最多9(視方案而定)" + ) + parser.add_argument( + "--return-url", + type=str, + help="ReturnURL(第一次付款回應)" + ) + parser.add_argument( + "--period-return-url", + type=str, + help="PeriodReturnURL(第2筆後每次付款回應)" + ) + parser.add_argument( + "--production", + type=lambda x: x.lower() == "true", + default=True, + help="生產環境:true=生產環境, false=測試環境" + ) + parser.add_argument( + "--version", + action="version", + version="1.0.0" + ) + + args = parser.parse_args() + + # 計算隨機性 ease... + if args.frequency is None or args.exec_times is None: + if args.period_type == "M": + args.frequency = 4 + args.exec_times = 4 + else: + args.frequency = 12 + args.exec_times = 12 + + return args + + +def main(): + """主程式""" + args = parse_cli_args() + + # 構建參數 + params = { + "MerchantID": args.merchant_id, + "MerchantTradeNo": args.merchant_trade_no, + "MerchantTradeDate": datetime.now().strftime("%Y/%m/%d %H:%M:%S"), + "PaymentType": "aio", + "TotalAmount": args.total_amount, + "TradeDesc": args.trade_desc, + "ItemName": args.item_name, + "ChoosePayment": "Credit", + "PeriodType": args.period_type, + "Frequency": args.frequency, + "ExecTimes": args.exec_times, + } + + # 添加回調 URL(如果提供) + if args.return_url: + params["ReturnURL"] = args.return_url + if args.period_return_url: + params["PeriodReturnURL"] = args.period_return_url + + # 生成 CheckMacValue + check_mac_value = generate_check_mac_value(params, args.hash_key, args.hash_iv) + + # 輸出參數 + print("=" * 80) + print("OMG Payment - 檢查參數生成結果") + print("=" * 80) + print(f"MerchantID: {params['MerchantID']}") + print(f"MerchantTradeNo: {params['MerchantTradeNo']}") + print(f"MerchantTradeDate: {params['MerchantTradeDate']}") + print(f"PaymentType: {params['PaymentType']}") + print(f"TotalAmount: {params['TotalAmount']}") + print(f"TradeDesc: {params['TradeDesc']}") + print(f"ItemName: {params['ItemName']}") + print(f"ChoosePayment: {params['ChoosePayment']}") + print(f"PeriodType: {params['PeriodType']}") + print(f"Frequency: {params['Frequency']}") + print(f"ExecTimes: {params['ExecTimes']}") + if "ReturnURL" in params: + print(f"ReturnURL: {params['ReturnURL']}") + if "PeriodReturnURL" in params: + print(f"PeriodReturnURL: {params['PeriodReturnURL']}") + + print(f"\nCheckMacValue (SHA256):") + print("-" * 80) + print(check_mac_value) + + print(f"\n完整 POST URL:") + print("-" * 80) + base_url = "https://payment.funpoint.com.tw" if args.production else "https://payment-stage.funpoint.com.tw" + print(f"{base_url}/Cashier/AioCheckOut/V5") + + print(f"\n表單資料(已 URL encode):") + print("-" * 80) + import urllib.parse + form_data = urllib.parse.urlencode(params, doseq=True) + print(form_data) + + print("=" * 80) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/skills/omg-payment/scripts/verify_payment.py b/skills/omg-payment/scripts/verify_payment.py new file mode 100644 index 00000000000..c8c4634f9ec --- /dev/null +++ b/skills/omg-payment/scripts/verify_payment.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" +OMG Payment - 付款通知驗證工具 + +使用方法: + python3 verify_payment.py --payment-no [選項] + +範例: + python3 verify_payment.py --payment-no "202602270001" + +驗證 stdin 輸入的 CheckMacValue 是否正確 +""" + +import hashlib +import sys +import argparse + + +# .NET URL Encoding 替換表格 +DOTNET_REPLACEMENTS = { + "%2d": "-", "%5f": "_", "%2e": ".", "%21": "!", "%2a": "*", "%28": "(", "%29": ")", +} + + +def generate_check_mac_value(params, hash_key, hash_iv): + """生成 OMG CheckMacValue(SHA256)""" + filtered = {k: v for k, v in params.items() if k != "CheckMacValue" and v} + sorted_keys = sorted(filtered.keys(), key=lambda k: k.lower()) + param_str = "&".join(f"{k}={filtered[k]}" for k in sorted_keys) + raw = f"{hash_key}&{param_str}&{hash_iv}" + encoded = quote(raw, safe="").lower() + for old, new in DOTNET_REPLACEMENTS.items(): + encoded = encoded.replace(old, new) + return hashlib.sha256(encoded.encode("utf-8")).hexdigest().upper() + + +def verify_payment_notification(params, hash_key, hash_iv): + """ + 驗證付款通知 + + Args: + params: 參數字典(來自 OMG 金流 POST) + hash_key: Hash Key + hash_iv: Hash IV + + Returns: + tuple: (is_valid, status_message) + + Raises: + Exception: CheckMacValue 不匹配時拋出 + """ + # 計算預期的 CheckMacValue + calculated_mac = generate_check_mac_value(params, hash_key, hash_iv) + server_mac = params.get("CheckMacValue", "").upper() + + # 比對簽名 + if calculated_mac != server_mac: + error_msg = f"CheckMacValue 不匹配!\n計算值: {calculated_mac}\n服務器值: {server_mac}" + raise Exception(error_msg) + + # 狀態檢查 + rtn_code = params.get("RtnCode", "") + rtn_msg = params.get("RtnMsg", "") + + if rtn_code != "1": + return False, f"支付失敗: {rtn_msg} (RtnCode: {rtn_code})" + + # 付款成功 + trade_no = params.get("TradeNo", "N/A") + trade_amt = params.get("TradeAmt", "N/A") + payment_date = params.get("PaymentDate", "N/A") + + return True, f"付款成功!\n交易編號: {trade_no}\n交易金額: {trade_amt} NT$\n付款時間: {payment_date}" + + +def parse_params_from_str(param_str): + """從字串解析參數""" + params = {} + for item in param_str.split("&"): + if "=" in item: + key, value = item.split("=", 1) + params[key] = value + return params + + +def main(): + parser = argparse.ArgumentParser( + description="驗證 OMG Payment 付款通知" + ) + + parser.add_argument( + "--hash-key", + type=str, + default="265fIDjIvesceXWM", + help="Hash Key" + ) + parser.add_argument( + "--hash-iv", + type=str, + default="pOOvhGd1V2pJbjfX", + help="Hash IV" + ) + parser.add_argument( + "--payment-no", + type=str, + help="付款編號(用於輸出)" + ) + + args = parser.parse_args() + + # 從 stdin 讀取參數 + param_str = sys.stdin.read().strip() + + if not param_str: + print("錯誤:未提供參數字串", file=sys.stderr) + sys.exit(1) + + # 解析參數 + params = parse_params_from_str(param_str) + + # 簽名都在 stdout 輸出(可以做測試驗證) + + try: + # 驗證 + is_valid, message = verify_payment_notification(params, args.hash_key, args.hash_iv) + + print("=" * 80) + if args.payment_no: + print(f"付款通知驗證結果:{args.payment_no}") + else: + print("付款通知驗證結果") + print("=" * 80) + + # 顯示參數摘要 + print("\n參數摘要:") + print("-" * 80) + for k, v in list(params.items())[:10]: # 只顯示前 10 個 + key_display = k if k != "CheckMacValue" else "CheckMacValue (驗證中...)" + print(f"{key_display}: {v}") + + if "CheckMacValue" in params: + print(f"\n服務器 CheckMacValue: {params['CheckMacValue']}") + + print("\n" + "=" * cat末自: + + # 發送 1|OK(如果測試) + + if not is_valid: + print(f"✗ 驗證失敗:{message}") + sys.exit(1) + else: + print(f"✓ 驗證成功!") + print(f"\n{message}") + print("\n(若要回應 OMG 金流,請發送: 1|OK)") + sys.exit(0) + + except Exception as e: + print("=" * 80) + print("✗ 驗證失敗") + print("=" * 80) + print(f"\n{str(e)}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file