原 ProviderV1::CallbackCoreV4 設計 # 1. 概述 ## 1-1. 概述 一般通稱 『單一錢包』『無縫錢包』, 由遊戲商方面提出 API 規格,由我方實作 遊戲商通常會分成『查詢餘額』『交易』兩種作業, 『交易』又分成「結算」「取消」兩種 只要我方最後 給予確認,此交易就成立 遊戲商都會要求設定 URL 用來呼叫我方的 API ## 1-2. SWT Flow `Provider` -> `Fatgame` -> `Merchant` -> `Fatgame` -> `Provider` ## 1-3. Flow 圖 ![](https://hackmd.io/_uploads/BkBsuT_j2.png) https://www.mermaidchart.com/app/projects/eedef130-4a27-4185-92f1-9591b7642442/diagrams/50705e2a-6591-4fef-9a56-48e0548a85c0/version/v0.1/edit ## 1-4. SWT#Swt::Transfter AASM 圖 ![](https://hackmd.io/_uploads/BJwWeAdoh.png) https://www.mermaidchart.com/app/projects/eedef130-4a27-4185-92f1-9591b7642442/diagrams/24001a30-db93-4ddd-a093-df96b0e076ea/version/v0.1/edit ## 1-5. SWT#MODEL 圖 ![](https://hackmd.io/_uploads/SydbKNzh2.png) https://www.mermaidchart.com/app/projects/eedef130-4a27-4185-92f1-9591b7642442/diagrams/eb783a40-8a03-465c-a843-c5f6bc51ab27/version/v0.1/edit # 2. 檔案配置 ## 2-1. SWT主核心 `app/services/provider_lib/swt/` - `core.rb` **# 主核心** - `config.rb` **# 預設控制項目** - `error.rb` **# 錯誤回收控制器** - `execute.rb` **# 主要入口 跟 模擬測試入口** - `fake_request.rb` **# 模擬遊戲商行為** ## 2-2. 流程控制 `app/services/provider_lib/swt/perform_of/` - `balance.rb` **# 餘額查詢** - `transfer.rb` **# 交易** - `query.rb` **# 其他查詢** ## 2-3. 輔助插件 `app/services/provider_lib/swt/plugin_of/` - `transfer_mapping.rb` **# 交易匹配** - `parse_build_json.rb` **# 解碼&回應** - `bet_addition.rb` - `arr_preview_balance.rb` ## 2-4. 遊戲商相關 SWT 入口 (用 hacksaw 做範例) `app/services/provider_v2/hacksaw/` - `swt.rb` **# 繼承 ProviderLib::Swt::Core** ## 2-5. 遊戲商相關 SWT 架構 (用 hacksaw 做範例) `app/services/provider_v2/hacksaw/swt/` - `config.rb` **# SWT 常數控制** - `valid_selector.rb` **# 選擇器** - `authenticate.rb` **# 遊戲商-實作功能** - `balance.rb` **# 遊戲商-實作功能** - `bet.rb` **# 遊戲商-實作功能** - `end_session.rb` **# 遊戲商-實作功能** - `promo_payout.rb` **# 遊戲商-實作功能** - `rollback.rb` **# 遊戲商-實作功能** - `win.rb` **# 遊戲商-實作功能** ## 2-6. Worker `app/worker/` - `swt_merchant_worker.rb` **# 商戶非同步** ## 2-7. Model `app/models/swt/` - `provider.rb` **# 遊戲商進出資料** - `transfer.rb` **# 注單控制** - `merchant.rb` **# 商戶進出資料** - `merchant_bet.rb` **# 商戶進出資料STI-Bet** - `merchant_cancel.rb` **# 商戶進出資料STI-Cancel** - `merchant_settle.rb` **# 商戶進出資料STI-Settle** - `merchant_resettle.rb` **# 商戶進出資料STI-Resettle** ## 2-8. Model/concerns `app/models/concerns/` - `find_init_upd.rb` **# 新增或更新** - `provider_helper.rb` **# Moele跟Provider交互關係** ## 2-9. API::JVD(for Merchant) `app/services/api_jvd/swt_v0/` - `base.rb` **# 共用** - `balance.rb` **# 查詢餘額** - `transfer.rb` **# 交易** ## 2-10. HTTP(RestClient) `app/services/api_jvd` - `common_v0.rb` **# RestClient 通訊 & Fake 通訊** ## 2-11. Lib輔助工具 `app/lib/` - `lib_data_mapping.rb` - `lib_init_arg_type.rb` - `lib_safe_const.rb` - `lib_tool_klass.rb` - `lib_tool_patch.rb` - `lib_tool_var.rb` - `lib_type_caller.rb` # 3. 程式結構流程 ## 3-1. Route/API-CALLBACK `config/routes/api.rb` ``` match ':uri_slug(/:uri_act(/:uri_opt1(/:uri_opt2)))', to: 'callback#perform', via: %i[get post] ``` ``` Example-1: https://api.lvh.me:9000/api/v4/callback/aabbccddeeff { uri_slug: 'aabbccddeeff', } Example-2: https://api.lvh.me:9000/api/v4/callback/aabbccddeeff/get_balance { uri_slug: 'aabbccddeeff', uri_act: 'get_balance', } Example-3: https://api.lvh.me:9000/api/v4/callback/aabbccddeeff/bet/Player8801 { uri_slug: 'aabbccddeeff', uri_act: 'bet', uri_opt1: 'Player8801', } ``` 根據 Provider 要求我方最多支援四個 URI/PATCH 參數 接收後傳至 Api::V4::CallbackController#perform 輸入到 ``` ProviderLib::Swt::Core.execute(request) ``` ## 3-2. <class>ProviderLib::Swt::Execute 針對 `uri_slug` 匹配出我方適合的 SecretKey 資料 初始化 Provider Callback 物件 ``` ProviderV2::Hacksaw::Callback.new(...).perform! ``` ## 3-3. <class>ProviderV2::Hacksaw::Swt 根據遊戲商需求 匹配適合的 parse_request 套件 ## 3-4. <module>ProviderLib::Swt::PluginOf::ParseBuildJson ## 3-4-1. <method>swt_parse_request ``` --> @req // <ActionDispatch::Request> <-- @req_url @req_headers @req_params @req_body ``` ## 3-5. <module>ProviderV2::Hacksaw::Swt::ValidSelector 選擇器,根據遊戲商的動作需求, 例如1: Bet, Rollback, Win, Balance 例如2: Bet, Cancel, Settle, Balance ## 3-5-1. <method>decode_request 解碼器 ``` --> @req_url @req_headers @req_params @req_body <-- @req_ip @req_time @req_sign ``` ## 3-5-2. <method>valid_selector 選擇器 ``` --> <-- @req_sign // sign or secret @user_account // PLAYER @user_token // PLAYER @req_action // Provider Action @valid_selector = { action: player: request: api_slug: ori_api_slug: } // for Swt::Provider attr ``` ## 3-5-3. <method>valid_of 驗證 ## 3-5-4. <method>action! 選擇需要 PerformOf 動作模式 | Provider Action | Perform Of | | -------- | -------- | | Balance | :balance | | Bet | :transfer | | Rollback | :transfer | | Cancel | :transfer | | Settle | :transfer | | Authenticate | :query | ## 3-6. <module>ProviderLib::Swt::PerformOf::Query 其他查詢 GOTO:3-9 ## 3-7. <module>ProviderLib::Swt::PerformOf::Balance 餘額查詢 GOTO:3-9 ## 3-8. <module>ProviderLib::Swt::PerformOf::Transfer 交易 針對『交易』的部分需要 Swt::* 的 model 控制 ## 3-8-1. <ActiveRecord>Swt::Provider..request request: 遊戲商傳來的資料,解碼後存放位置 ## 3-8-2. <ActiveRecord>Swt::Transfer..AASM AASM: bet! (`init` -> `betting`) ## 3-8-3. <ActiveRecord>Swt::Merchant..request request: 將 Swt::Swt::Transfer ,轉換成 Swt::Merchant..request ApiJvd::SwtV0::Transfer ## 3-8-4. <ActiveRecord>Swt::Merchant..respond respond: ApiJvd::SwtV0::Transfer 回傳存到 Swt::Merchant..respond ## 3-8-5. <ActiveRecord>Swt::Transfer..AASM AASM: bet_ok! (`betting` -> `betted`) ## 3-8-6. <ActiveRecord>Swt::Provider..respond respond: 回傳存到 Swt::Provider..respond ## 3-9. <module>ProviderLib::Swt::PluginOf::ParseBuildJson ## 3-9-1. <method>swt_build_response # 4. Fatgame SWT 錯誤處理 ## 4-1. <class>ProviderLib::Swt::Error 錯誤處理 主要物件 ## 4-2. <module>ProviderLib::Swt::Config 錯誤處理 預設處理方式 ## 4-3. <module>ProviderV2::Hacksaw::Swt::Config 遊戲商 參數代碼 錯誤代碼 錯誤代碼對照表 ## 4-4. Fatgame SWT 錯誤處理代碼 ## 4-4-1. invalid_token 無效令牌 無法驗證網站或是玩家 ## 4-4-2. invalid_sign 無效SIGN 驗證 加密後的 sign 是否正確 ## 4-4-3. invalid_ip 無效IP 非白名單IP ## 4-4-4. invalid_time 無效時間 如果有需要驗證時間 ## 4-4-5. invalid_slug 無效 API 唯一值 一般交易型的資料都有一個唯一 ID 確保不會錯認資料 ## 4-4-6. invalid_action 無效動作 有戲商指定的 Action 無效 ## 4-4-7. invalid_content 無效內容 內容無法解析 ## 4-4-8. player_not_exist 玩家不存在 Fatgame User 找不到帳號 ## 4-4-9. player_banned 玩家被禁止 Merchant 拒絕此玩家 ## 4-4-10. invalid_bet 無效投注 Swt::Transfter AASM 防禦 ## 4-4-11. bet_num_executed 注單已執行 Merchant 已執行過此動作 ## 4-4-12. bet_num_not_exist 注單不存在 此注單並未在系統中 ## 4-4-13. balance_insufficient 餘額不足(for merchant) Merchant 玩家餘額不足 ## 4-4-14. system_maintenance 系統維護(for merchant) Merchant 系統維護 ## 4-4-15. unable_to_connect 無法連線(for merchant) Merchant 無法連線 ## 4-4-16. connection_timeout 連線逾時(for merchant) Merchant 連線逾時 ## 4-4-17. data_format_error 資料格式錯誤(for merchant) Merchant 資料格式錯誤 ## 4-4-18. exception 其他例外 未預期其他例外 ## 4-5. 遊戲商 狀態代碼 (Error) ## 4-5-1. 遊戲商給的 Error Code Mapping 以下為遊戲商給的範例 ``` 1 ==> General/Server error (Provider rollback) 2 ==> Invalid user / token expired 3 ==> Invalid currency for user 4 ==> Invalid partner code 5 ==> Insufficient funds to place bet 6 ==> Account locked 7 ==> Account disabled 8 ==> Gambling limit exceeded (Loss limit or betting limit) 9 ==> Time limit exceeded 10 ==> Session timeout or invalid session id 11 ==> General error, but no rollback required. Use this if you know for a fact that a bet was not logged in your database due to a deterministic error and you do not require the round to be rolled back. 12 ==> Invalid action. Please return this code if you get a request type that you don't recognize. It will be treated as OK by us, and not retried. This allows us to add new request types without affecting current integrations. ``` ## 4-5-2. Setup Provider Config ### File Site `app/services/provider_v2/<provider_name>/swt/config.rb` ### <const>PROVIDER_ERROR_MAPPING Fatgame 與 遊戲商 的 ERROR CODE 對應表 若註解掉 key 則不檢查此項目 PROVIDER_ERROR_MAPPING key ==> Fatgame Error Code value ==> Provider Error Code ## 4-6. 遊戲商 狀態代碼 (Success) ## 4-6-1. 遊戲商給的 Success Code statusCode: 0 ## 4-6-2. Setup Provider Config (Success) ### File Site `app/services/provider_v2/<provider_name>/swt/config.rb` ## 4-6-3. <const>PROVIDER_SUCCESS_CODE 遊戲商的 SUCCESS 代碼 ## 4-6-4. <const>PROVIDER_SUCCESS_MSG 成功訊息 ## 4-7. 動態 valid_of_* 方法 `app/services/provider_lib/swt/config.rb#143` 在 core 中 會見到 valid_of_* 這就是就是 mapping error rule 的檢查機制 # 5. 參數說明&自訂 PROC ## 5-1. 常數表 ## 5-2. 自訂 PROC 如果有需要就使用 ## 5-2-1. <proc>ERROR_RULE {必要} 自訂錯誤處理原則(如果沒有需要則保留空 Hash) ==> 預設原則參考 app/services/provider_v2/swt_config ## 5-2-2. <proc>PROC_PROVIDER_SUCCESS {推薦使用} 統一處理 provider success 需要的回傳格式&內容 ## 5-2-3. <proc>PROC_PROVIDER_ERROR {推薦使用} 統一處理 provider error 需要的回傳格式&內容 ## 5-2-4. <proc>PROC_AMOUNT_TRANSFER {遊戲商需求} 百分位金額 轉換成 金額(從遊戲商接收) ## 5-2-5. <proc>PROC_AMOUNT_RETURN {遊戲商需求} 金額 轉換成 百分位金額(返回遊戲商) ## 5-2-6. <proc>PROC_SWT_TO_HISTORY {遊戲商需求} 部分遊戲商需要從 Swt::Transfter 提取 BetLog 記錄 ## 5-3. 動態 @* 方法 轉 var_* 外部使用 instance_variable_set 參數 hacksaw = ProviderV2::Hacksaw::Swt.new(...) hacksaw 內部有個 instance_variable_set 是 @req hacksaw.var_req # 6. 插件 ## 6-1. <module>ProviderLib::Swt::PluginOf::ParseBuildJson Plugin 用來處理 HTTP Parse And Build for Json Format ### 6-1-1. swt_parse_request Parse Request(Http) to Hash ``` --> @req <ActionDispatch::Request> <-- { url: ..., headers: ..., params: ..., body: ..., } ``` ### 6-1-2. swt_build_response Build Response For Controller render args ``` --> @res <Hash>(with JSON formatter) <-- { json: ... } ``` ## 6-2. <module>ProviderLib::Swt::PluginOf::TransferMapping Swt::Transfer 的 mapping ## 6-3. <module>ProviderLib::Swt::PluginOf::ArrPreviewBalance 用來處理 預作 users 的 balance 結果 ## 6-4. <module>ProviderLib::Swt::PluginOf::BetAddition (未完成)用來處理 Bet 加注行為 # 7. 工具包 ## 7-1. 通用工具包 ## 7-1-1. <module>LibDataMapping Hash 資料對應 CONST 來處理 mapping 轉換成新的 Hash ## 7-1-2. <module>LibInitArgType 自動轉換 initialize 的 args 參數對應初始的 dnyamic method (init_arg_of_xxx) ## 7-1-3. <module>LibSafeConst 動態用來處理外部 class 當成 config const 來源 ## 7-1-4. <module>LibToolKlass 1. parser_klass_name 取出 Class Name 變成 symbol downcase 2. klass_serializer 序列化 Class Name + ID 為 "ClassName#1" 3. klass_deserializer 反序列化 "ClassName#1" 為 ClassName.find(ID) ## 7-1-5. <module>LibToolPatch 1. PROC_TO_ARRAY 不是 Array 就轉化為用 Array 包裝 2. PROC_CUSTOM_HASH_SORT 自訂排列 3. PROC_CUSTOM_ARRAY_SORT 自訂排列 4. PROC_INIT_DEEP_HASH 可以直接使用 無限層 Hash,無需初始化 ## 7-1-6. <module>LibToolVar 1. build_variable_set 將 Hash 轉換成 @key = value 2. fetch_variable_get 取得 @key & value 的值轉回 Hash 3. find_var (未完成,待修改) ## 7-1-7. <module>LibTypeCaller var 的 class name,動態呼叫 method # 8. SEPC架構 單一錢包的架構較為複雜,fatgame處於中間層,當遊戲商發送API請求後,fatgame收到後須在發送API至包網端詢問此玩家帳戶餘額,待包網響應結果後,fatgame再依照包網的回應處理後response給遊戲商。 使用 yml 設定檔進行測試,可以模擬預測的包網回應行為及我方收到後的回應結果是否如預期。 測試方式 ``` ProviderLib::Swt::Core.execute_fake(:hacksaw, '20') # 測試範本 spec/fixtures/files/provider_v2/hacksaw/swt/20_bet.json ``` ```ruby # 新單一錢包 RSpec entrypoint spec/service/provider_v2/swt/common_spec.rb ``` ### yml結構大致分為 1. API參數設定 2. 包網回應行為 3. 回應遊戲商結果 4. 驗證資料正確性 以hacksaw遊戲舉例,下方為測試的yml,檔名命名規範為 數字 加上 測試行為 e.g. 11_test_for_demo.yml ![](https://hackmd.io/_uploads/SkrO2hufT.png =300x) ### 00 開頭為db資料配置 此設定檔為測試所虛假資料建置,key 對應到的是 Model的名稱,接著是欄位,以此類推..。 ```jsonld --- Merchant: id: 1 name: Rspec測試站 account: rspec_testing currency: PHP callback_info: query_url: '' balance_url: '' transfer_url: '' multiple_transfer_url: '' SecretKey: id: 1 name: hacksaw - hacksaw testing is_active: true slug: vsnaw1mejevkwhun game_id: 112 value: api_pw: TsTHsw*fi api_un: jvd_test secret: Test+RsPec-only partner: JVD host_url: http://hacksaw-test.111fatgame.com lobby_url: https://fatcat-dev.1a2b333.com/ script_url: "//static-stg.hacksawgaming.com/launcher/hacksaw-test.min.js" MerchantGame: id: 1 merchant_id: 1 game_id: 112 name: hacksaw Rspec test User: secret_key_id: 1 merchant_game_id: 1 platform_uid: 99 account: RSPECtest01 status: active token: 1A2s3d4F5 balance: 200 ``` ### 情境測試(常規) yml format ```jsonld --- '0': desc: '' '1': desc: '' url: https://xxx.com.tw/ method: POST body: mock_fake: merchant: code: expects: valid_structure: type: Array body: [] provider_response: - json: ``` 0為此情境的描述說明,依照數字的順序執行 desc ⇒ 行為說明 url ⇒ 發送API url method ⇒ http action body ⇒ 參數內容 mock_fake ⇒ 包網回應的結果,可分為兩種回應條件 1. 回傳 code ![](https://hackmd.io/_uploads/HJMOa2OGa.png =300x) 狀態會對應error code並回應對應的 response format 2. 回應 response body ![](https://hackmd.io/_uploads/H1m3p3_G6.png =300x) 可自行定義回應的內容 expects ⇒ 驗證預期得到結果 ![](https://hackmd.io/_uploads/Bk5_ChOza.png =300x) 如不需驗證此action的結果 expects 帶預設格式即可 - valid_structure: type 有 Array 與 Hash 兩種 Array 使用 eval 方式執行 Hash 待定 - provider_response: 驗證 API response body ################ 下注的情境測試內容 ################ ```jsonld --- '0': desc: '情境 - 下注,包網響應成功,回傳成功狀態並驗證 model 資料正確性' '1': desc: 'Bet - success' url: https://api.lvh.me:9000/api/v4/callback/vsnaw1mejevkwhun method: POST body: action: Bet amount: 500 gameId: 1085 secret: Test+RsPec-only roundId: 5000227711868 currency: PHP gameSessionId: 5000008737813 transactionId: 5000453688754 externalPlayerId: RSPECtest01 externalSessionId: '' mock_fake: merchant: return: ikey: '0000000000000001' status: OK account: RSPECtest01 balance: 195 currency: PHP http_code: 200 timestamp: '2023-09-01 18:11:40 +0800' expects: valid_structure: type: Array body: - "@swt_transfer = Swt::Transfer.find_by(merchant_game_id: 1, bet_num: '5000227711868_5000453688754')" - expect(@swt_transfer.aasm_state).to eq('betted') - expect(@swt_transfer.bet_amount).to eq(5.0) provider_response: - json: statusCode: 0 statusMessage: '' accountBalance: 19500 ``` ### 情境測試(併發) 可模擬併發時的情境,驗證回應給遊戲商的response 和資料的正確性 使用方式: 以下方yml檔為例,數字 1開頭的 可以看到有 1.1, 1.2,系統將它歸類在同一組並同時間發送測試請求。 ```jsonld --- '0': desc: '情境 - 測試下注 race condition 情境,驗證 model 資料正確性' '1.1': desc: 'Bet' url: https://api.lvh.me:9000/api/v4/callback/vsnaw1mejevkwhun method: POST body: action: Bet amount: 500 gameId: 1085 secret: Test+RsPec-only roundId: 5000227711868 currency: PHP gameSessionId: 5000008737813 transactionId: 5000453688754 externalPlayerId: RSPECtest01 externalSessionId: '' mock_fake: merchant: return: ikey: '0000000000000001' status: OK account: RSPECtest01 balance: 195 currency: PHP http_code: 200 timestamp: '2023-09-01 18:11:40 +0800' expects: valid_structure: type: Array body: [] '1.2': desc: 'Bet' url: https://api.lvh.me:9000/api/v4/callback/vsnaw1mejevkwhun method: POST body: action: Bet amount: 500 gameId: 1085 secret: Test+RsPec-only roundId: 5000227711868 currency: PHP gameSessionId: 5000008737813 transactionId: 5000453688754 externalPlayerId: RSPECtest01 externalSessionId: '' expects: valid_structure: type: Array body: [] '2': desc: 'valid Model' expects: valid_structure: type: Array body: - "@swt_merchant = Swt::Merchant.find_by(merchant_game_id: 1, bet_num: '5000227711868_5000453688754')" - "@swt_transfer = Swt::Transfer.find_by(merchant_game_id: 1, bet_num: '5000227711868_5000453688754')" - "expect(@swt_transfer.aasm_state).to eq('betting') if @swt_merchant.aasm_state == 'fail'" - "expect(@swt_transfer.aasm_state).to eq('betted') if @swt_merchant.aasm_state == 'success'" - "expect(@user.reload.balance).to eq(200.0) if @swt_merchant.aasm_state == 'fail'" - "expect(@user.reload.balance).to eq(195.0) if @swt_merchant.aasm_state == 'success'" - "expect(Swt::Transfer.count).to eq(1)" ``` # 9. Debug