diff --git a/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_exec_tran_with_3DS2_0_returns_3DS2_0_challenge_flow_response.yml b/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_exec_tran_with_3DS2_0_returns_3DS2_0_challenge_flow_response.yml new file mode 100644 index 0000000..51f9fe0 --- /dev/null +++ b/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_exec_tran_with_3DS2_0_returns_3DS2_0_challenge_flow_response.yml @@ -0,0 +1,33 @@ +--- +http_interactions: +- request: + method: post + uri: https:///payment/ExecTran.idPass + body: + encoding: UTF-8 + string: AccessID=&AccessPass=&OrderID=&Method=1&PayTimes=&MemberID=&CardSeq=0&TdsType=2&Tds2Type=2&CallBackUrl=https%3A%2F%2Fexample.com%2Fcallback&Tds2RetUrl=https%3A%2F%2Fexample.com%2F3ds%2Freturn + headers: + Accept: + - "*/*" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - text/plain; charset=shift_jis + Date: + - Thu, 24 Jul 2025 23:15:18 GMT + Server: + - Apache + body: + encoding: UTF-8 + string: OrderID=&Forward=&Method=1&PayTimes=&Approve=&TranID=&TranDate=&CheckString=&Tds2TransResult=C&Tds2TransResultReason=01&Tds2ChallengeUrl=https:///payment/Tds2Challenge?tds2TransID=&Tds2TransID= + recorded_at: Thu, 24 Jul 2025 23:15:18 GMT +recorded_with: VCR 6.3.1 \ No newline at end of file diff --git a/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_secure_tran_2_completes_payment_after_3DS2_0_challenge_verification.yml b/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_secure_tran_2_completes_payment_after_3DS2_0_challenge_verification.yml new file mode 100644 index 0000000..3191fe4 --- /dev/null +++ b/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_secure_tran_2_completes_payment_after_3DS2_0_challenge_verification.yml @@ -0,0 +1,33 @@ +--- +http_interactions: +- request: + method: post + uri: https:///payment/SecureTran2.idPass + body: + encoding: UTF-8 + string: AccessID=&AccessPass= + headers: + Accept: + - "*/*" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - text/plain; charset=shift_jis + Date: + - Thu, 24 Jul 2025 23:15:18 GMT + Server: + - Apache + body: + encoding: UTF-8 + string: OrderID=&Forward=&Method=1&PayTimes=&Approve=&TranID=&TranDate=&CheckString= + recorded_at: Thu, 24 Jul 2025 23:15:18 GMT +recorded_with: VCR 6.3.1 \ No newline at end of file diff --git a/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_tds2_auth_executes_3DS2_0_authentication.yml b/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_tds2_auth_executes_3DS2_0_authentication.yml new file mode 100644 index 0000000..06b3cb1 --- /dev/null +++ b/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_tds2_auth_executes_3DS2_0_authentication.yml @@ -0,0 +1,33 @@ +--- +http_interactions: +- request: + method: post + uri: https:///payment/Tds2Auth.idPass + body: + encoding: UTF-8 + string: AccessID=&AccessPass=&Tds2Param=&ShopID=&ShopPass= + headers: + Accept: + - "*/*" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - text/plain; charset=shift_jis + Date: + - Thu, 24 Jul 2025 23:15:18 GMT + Server: + - Apache + body: + encoding: UTF-8 + string: OrderID=&Forward=&Method=1&PayTimes=&Approve=&TranID=&TranDate=&CheckString=&Tds2TransResult=Y&Tds2TransResultReason=&Tds2ChallengeUrl=https:///payment/Tds2Challenge?tds2TransID= + recorded_at: Thu, 24 Jul 2025 23:15:18 GMT +recorded_with: VCR 6.3.1 \ No newline at end of file diff --git a/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_tds2_result_gets_3DS2_0_authentication_result.yml b/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_tds2_result_gets_3DS2_0_authentication_result.yml new file mode 100644 index 0000000..7fb364e --- /dev/null +++ b/fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_tds2_result_gets_3DS2_0_authentication_result.yml @@ -0,0 +1,33 @@ +--- +http_interactions: +- request: + method: post + uri: https:///payment/Tds2Result.idPass + body: + encoding: UTF-8 + string: AccessID=&AccessPass=&ShopID=&ShopPass= + headers: + Accept: + - "*/*" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - text/plain; charset=shift_jis + Date: + - Thu, 24 Jul 2025 23:15:18 GMT + Server: + - Apache + body: + encoding: UTF-8 + string: OrderID=&Forward=&Method=1&PayTimes=&Approve=&TranID=&TranDate=&CheckString=&Tds2TransResult=Y&Tds2TransResultReason= + recorded_at: Thu, 24 Jul 2025 23:15:18 GMT +recorded_with: VCR 6.3.1 \ No newline at end of file diff --git a/lib/gmo.rb b/lib/gmo.rb index f6dc1d2..02f814e 100644 --- a/lib/gmo.rb +++ b/lib/gmo.rb @@ -48,6 +48,13 @@ def api(path, args = {}, verb = "post", options = {}, &error_checking_block) key_values = result.body.to_s.split('&').map { |str| str.split('=', 2) }.flatten response = Hash[*key_values] end + # Transform the redirect_url + # "ACS=2&RedirectUrl=https://manage.tds2gw.gmopg.jp/api/v2/brw/callback?transId=6e48e31f-2940-48e1-a702-ebba2f3373ee&t=dccc8a7ed85372c9accff576bff59b3a" + # => { "ACS" => "2", RedirectUrl => "https://manage.tds2gw.gmopg.jp/api/v2/brw/callback?transId=6e48e31f-2940-48e1-a702- ebba2f3373ee&t=dccc8a7ed85372c9accff576bff59b3a" } + if response['RedirectUrl'] && response['RedirectUrl'] != '' && response['t'] && response['t'] != '' && response.keys.index('RedirectUrl') + 1 == response.keys.index('t') + response['RedirectUrl'] = response['RedirectUrl'] + '&t=' + response['t'] + response.delete('t') + end # converting to UTF-8 body = response = Hash[response.map { |k,v| [k, NKF.nkf('-S -w',v)] }] # Check for errors if provided a error_checking_block diff --git a/lib/gmo/const.rb b/lib/gmo/const.rb index e16fc03..218c355 100644 --- a/lib/gmo/const.rb +++ b/lib/gmo/const.rb @@ -37,11 +37,13 @@ module Const :auth_code_2 => "Auth_Code2", :auth_code_3 => "Auth_Code3", :amount => "Amount", + :app_mode => "AppMode", :bank_code => "Bank_Code", :bank_id => "Bank_ID", :branch_code => "Branch_Code", :branch_code_jp => "Branch_Code_Jpbank", :call_back_url => "Call_Back_Url", + :callback_type => "CallbackType", :cancel_amount => "CancelAmount", :cancel_tax => "CancelTax", :card_name => "CardName", @@ -156,7 +158,55 @@ module Const :suica_add_info_4 => "SuicaAddInfo4", :tax => "Tax", :td_flag => "TdFlag", + :tds2_type => "Tds2Type", + :td_required => "TdRequired", :td_tenant_name => "TdTenantName", + :tds2_ch_acc_change => "Tds2ChAccChange", + :tds2_ch_acc_date => "Tds2ChAccDate", + :tds2_ch_acc_pw_change => "Tds2ChAccPwChange", + :tds2_nb_purchase_account => "Tds2NbPurchaseAccount", + :tds2_param => "Tds2Param", + :tds2_payment_acc_age => "Tds2PaymentAccAge", + :tds2_provision_attempts_day => "Tds2ProvisionAttemptsDay", + :tds2_ship_address_usage => "Tds2ShipAddressUsage", + :tds2_ship_name_ind => "Tds2ShipNameInd", + :tds2_suspicious_acc_activity => "Tds2SuspiciousAccActivity", + :tds2_txn_activity_day => "Tds2TxnActivityDay", + :tds2_txn_activity_year => "Tds2TxnActivityYear", + :tds2_3ds_req_auth_data => "Tds2ThreeDSReqAuthData", + :tds2_3ds_req_auth_method => "Tds2ThreeDSReqAuthMethod", + :tds2_3ds_req_auth_timestamp => "Tds2ThreeDSReqAuthTimestamp", + :tds2_addr_match => "Tds2AddrMatch", + :tds2_bill_addr_city => "Tds2BillAddrCity", + :tds2_bill_addr_country => "Tds2BillAddrCountry", + :tds2_bill_addr_line1 => "Tds2BillAddrLine1", + :tds2_bill_addr_line2 => "Tds2BillAddrLine2", + :tds2_bill_addr_line3 => "Tds2BillAddrLine3", + :tds2_bill_addr_post_code => "Tds2BillAddrPostCode", + :tds2_bill_addr_state => "Tds2BillAddrState", + :tds2_email => "Tds2Email", + :tds2_home_phone_cc => "Tds2HomePhoneCC", + :tds2_home_phone_subscriber => "Tds2HomePhoneSubscriber", + :tds2_mobile_phone_cc => "Tds2MobilePhoneCC", + :tds2_mobile_phone_subscriber => "Tds2MobilePhoneSubscriber", + :tds2_work_phone_cc => "Tds2WorkPhoneCC", + :tds2_work_phone_subscriber => "Tds2WorkPhoneSubscriber", + :tds2_ship_addr_city => "Tds2ShipAddrCity", + :tds2_ship_addr_country => "Tds2ShipAddrCountry", + :tds2_ship_addr_line1 => "Tds2ShipAddrLine1", + :tds2_ship_addr_line2 => "Tds2ShipAddrLine2", + :tds2_ship_addr_line3 => "Tds2ShipAddrLine3", + :tds2_ship_addr_post_code => "Tds2ShipAddrPostCode", + :tds2_ship_addr_state => "Tds2ShipAddrState", + :tds2_delivery_email_address => "Tds2DeliveryEmailAddress", + :tds2_delivery_time_frame => "Tds2DeliveryTimeframe", + :tds2_gift_card_amount => "Tds2GiftCardAmount", + :tds2_gift_card_count => "Tds2GiftCardCount", + :tds2_gift_card_curr => "Tds2GiftCardCurr", + :tds2_pre_order_date => "Tds2PreOrderDate", + :tds2_pre_order_purchase_ind => "Tds2PreOrderPurchaseInd", + :tds2_reorder_items_ind => "Tds2ReorderItemsInd", + :tds2_ship_ind => "Tds2ShipInd", :tel_no => "TelNo", :token => "Token", :token_seq => "TokenSeq", diff --git a/lib/gmo/shop_api.rb b/lib/gmo/shop_api.rb index 6d88df1..31d50f8 100644 --- a/lib/gmo/shop_api.rb +++ b/lib/gmo/shop_api.rb @@ -691,6 +691,39 @@ def search_trade_multi(options = {}) post_request name, options end + ################################################### + # 3DS2.0 対応 + ################################################### + + # 4.3.1.7 3DS2.0認証実行(Tds2Auth) + # DS2.0認証を実行します。 + # 3DS2.0認証初期化URL(RedirectUrl)のコールバックを受けたタイミングで本処理を実行してください。 + def tds2_auth(options = {}) + name = "Tds2Auth.idPass" + required = [:access_id, :access_pass, :tds2_param] + assert_required_options(required, options) + post_request name, options + end + + # 4.3.1.8 3DS2.0認証結果取得(Tds2Result) + # 3DS2.0認証の最終的な認証結果を取得します。 + # 3DS2.0認証チャレンジURL(ChallengeUrl)のコールバックを受けたタイミングで本処理を実行してください。 + def tds2_result(options = {}) + name = "Tds2Result.idPass" + required = [:access_id, :access_pass] + assert_required_options(required, options) + post_request name, options + end + + # 4.3.1.12 3DS2.0認証後決済実行(SecureTran2) + # 3DS2.0サービスの結果を解析し、その情報を使用してカード会社と通信を行い決済を実施して結果を返します。 + def secure_tran_2(options = {}) + name = "SecureTran2.idPass" + required = [:access_id, :access_pass] + assert_required_options(required, options) + post_request name, options + end + private def api_call(name, args = {}, verb = "post", options = {}) diff --git a/spec/gmo/shop_api_spec.rb b/spec/gmo/shop_api_spec.rb index 659afff..16b1102 100644 --- a/spec/gmo/shop_api_spec.rb +++ b/spec/gmo/shop_api_spec.rb @@ -1192,4 +1192,94 @@ end end + describe "#exec_tran with 3DS2.0" do + it "returns 3DS2.0 challenge flow response", :vcr do + result = @service.exec_tran({ + :access_id => "test_access_7ed782e0e10604b31258e0d69ee3e887", + :access_pass => "test_pass_7be5e8786e2e959d7b48ad708634dc7e", + :order_id => "TEST_3DS_20250724231518", + :method => 1, + :pay_times => "", + :card_no => "4111111111111111", + :expire => "2512", + :member_id => "TEST_MEMBER_5df458bc", + :card_seq => 0, + :tds_type => 2, + :tds2_type => 2, + :call_back_url => "https://example.com/callback", + :tds2_ret_url => "https://example.com/3ds/return" + }) + + result["OrderID"].should == "" + result["Forward"].should == "" + result["Method"].should == "1" + result["TranID"].should == "" + result["TranDate"].should == "" + result["Tds2TransResult"].should == "C" + result["Tds2TransResultReason"].should == "01" + result["Tds2ChallengeUrl"].should include("/payment/Tds2Challenge") + result["Tds2TransID"].should == "" + end + end + + describe "#tds2_auth" do + it "executes 3DS2.0 authentication", :vcr do + result = @service.tds2_auth({ + :access_id => "test_access_7ed782e0e10604b31258e0d69ee3e887", + :access_pass => "test_pass_7be5e8786e2e959d7b48ad708634dc7e", + :tds2_param => "dummy_tds2_param" + }) + + result["OrderID"].should == "" + result["Forward"].should == "" + result["Method"].should == "1" + result["PayTimes"].should == "" + result["Approve"].should == "" + result["TranID"].should == "" + result["TranDate"].should == "" + result["CheckString"].should == "" + result["Tds2TransResult"].should == "Y" # Authentication successful + result["Tds2TransResultReason"].should == "" + result["Tds2ChallengeUrl"].should_not be_nil + result["Tds2ChallengeUrl"].should include("/payment/Tds2Challenge") + end + end + + describe "#tds2_result" do + it "gets 3DS2.0 authentication result", :vcr do + result = @service.tds2_result({ + :access_id => "test_access_7ed782e0e10604b31258e0d69ee3e887", + :access_pass => "test_pass_7be5e8786e2e959d7b48ad708634dc7e" + }) + + result["OrderID"].should == "" + result["Forward"].should == "" + result["Method"].should == "1" + result["PayTimes"].should == "" + result["Approve"].should == "" + result["TranID"].should == "" + result["TranDate"].should == "" + result["CheckString"].should == "" + result["Tds2TransResult"].should == "Y" # Authentication successful + result["Tds2TransResultReason"].should == "" + end + end + + describe "#secure_tran_2" do + it "completes payment after 3DS2.0 challenge verification", :vcr do + result = @service.secure_tran_2({ + :access_id => "test_access_7ed782e0e10604b31258e0d69ee3e887", + :access_pass => "test_pass_7be5e8786e2e959d7b48ad708634dc7e" + }) + + result["OrderID"].should == "" + result["Forward"].should == "" + result["Method"].should == "1" + result["PayTimes"].should == "" + result["Approve"].should == "" + result["TranID"].should == "" + result["TranDate"].should == "" + result["CheckString"].should == "" + end + end end