原 ProviderV1::CallbackCoreV4 設計
# 1. 概述
## 1-1. 概述
一般通稱 『單一錢包』『無縫錢包』,
由遊戲商方面提出 API 規格,由我方實作
遊戲商通常會分成『查詢餘額』『交易』兩種作業,
『交易』又分成「結算」「取消」兩種
只要我方最後 給予確認,此交易就成立
遊戲商都會要求設定 URL 用來呼叫我方的 API
## 1-2. SWT Flow
`Provider` -> `Fatgame` -> `Merchant` -> `Fatgame` -> `Provider`
## 1-3. Flow 圖

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://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://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

### 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

狀態會對應error code並回應對應的 response format
2. 回應 response body

可自行定義回應的內容
expects ⇒ 驗證預期得到結果

如不需驗證此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