From 41b1456764d4034eb22383ec82ddad59063ec59e Mon Sep 17 00:00:00 2001 From: jusco Date: Thu, 24 Feb 2022 22:20:16 +0900 Subject: [PATCH 1/8] fix 3DS response --- lib/gmo.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/gmo.rb b/lib/gmo.rb index f6dc1d2..2375883 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'].present? && response['t'].present? && 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 From 8c6aa93ed3db52f5b1dcfa56dd25a9d38f2c7ab7 Mon Sep 17 00:00:00 2001 From: jusco Date: Thu, 17 Feb 2022 17:45:18 +0900 Subject: [PATCH 2/8] add secure_tran_2 --- lib/gmo/shop_api.rb | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) 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 = {}) From 2d7f37dd67a69e38dfaab2a19700d3f578ef5648 Mon Sep 17 00:00:00 2001 From: jusco Date: Wed, 9 Feb 2022 19:02:32 +0900 Subject: [PATCH 3/8] fix typo --- lib/gmo/const.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gmo/const.rb b/lib/gmo/const.rb index e16fc03..c138540 100644 --- a/lib/gmo/const.rb +++ b/lib/gmo/const.rb @@ -137,7 +137,7 @@ module Const :remit_method_bank => "Remit_Method_Bank", :remit_method_sevenatm => "Remit_Method_Sevenatm", :reserve_no => "ReserveNo", - :ret_url => "RetURL", + :ret_url => "RetUrl", :security_code => "SecurityCode", :select_key => "Select_Key", :seq_mode => "SeqMode", From b8fda76f94c34effb99652ae8aab2e24f94b5f81 Mon Sep 17 00:00:00 2001 From: jusco Date: Wed, 9 Feb 2022 16:23:14 +0900 Subject: [PATCH 4/8] add const for input_params --- lib/gmo/const.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/gmo/const.rb b/lib/gmo/const.rb index c138540..9e87af2 100644 --- a/lib/gmo/const.rb +++ b/lib/gmo/const.rb @@ -156,6 +156,8 @@ module Const :suica_add_info_4 => "SuicaAddInfo4", :tax => "Tax", :td_flag => "TdFlag", + :tds2_type => "Tds2Type", + :td_required => "TdRequired", :td_tenant_name => "TdTenantName", :tel_no => "TelNo", :token => "Token", From 3d195ab2bce9bc330ba88a6ec2f53977f69fbae7 Mon Sep 17 00:00:00 2001 From: jusco Date: Wed, 9 Feb 2022 02:27:18 +0900 Subject: [PATCH 5/8] =?UTF-8?q?3DS2.0=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/gmo/const.rb | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/lib/gmo/const.rb b/lib/gmo/const.rb index 9e87af2..39926a4 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", @@ -159,6 +161,52 @@ module Const :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", From bb99605b748d14ddd305b2ee51a53f91230bba0e Mon Sep 17 00:00:00 2001 From: jiikko Date: Thu, 24 Jul 2025 22:08:51 +0900 Subject: [PATCH 6/8] Revert "fix typo" This reverts commit 07cc17be1c51b765d1d8be1439a162547964054d. --- lib/gmo/const.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gmo/const.rb b/lib/gmo/const.rb index 39926a4..218c355 100644 --- a/lib/gmo/const.rb +++ b/lib/gmo/const.rb @@ -139,7 +139,7 @@ module Const :remit_method_bank => "Remit_Method_Bank", :remit_method_sevenatm => "Remit_Method_Sevenatm", :reserve_no => "ReserveNo", - :ret_url => "RetUrl", + :ret_url => "RetURL", :security_code => "SecurityCode", :select_key => "Select_Key", :seq_mode => "SeqMode", From 544d2bce440ac19d4e6964b294038f379630eee7 Mon Sep 17 00:00:00 2001 From: jiikko Date: Thu, 24 Jul 2025 22:31:07 +0900 Subject: [PATCH 7/8] =?UTF-8?q?present=3F=E3=83=A1=E3=82=BD=E3=83=83?= =?UTF-8?q?=E3=83=89=E3=81=AE=E4=BD=BF=E7=94=A8=E3=82=92=E6=A8=99=E6=BA=96?= =?UTF-8?q?=E7=9A=84=E3=81=AARuby=E3=81=AE=E6=9D=A1=E4=BB=B6=E5=BC=8F?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/gmo.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gmo.rb b/lib/gmo.rb index 2375883..02f814e 100644 --- a/lib/gmo.rb +++ b/lib/gmo.rb @@ -51,7 +51,7 @@ def api(path, args = {}, verb = "post", options = {}, &error_checking_block) # 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'].present? && response['t'].present? && response.keys.index('RedirectUrl') + 1 == response.keys.index('t') + 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 From eaf3e0c6289440e94bf95b2fc2d094c049b99faa Mon Sep 17 00:00:00 2001 From: jiikko Date: Tue, 29 Jul 2025 21:29:41 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...returns_3DS2_0_challenge_flow_response.yml | 33 +++++++ ...nt_after_3DS2_0_challenge_verification.yml | 33 +++++++ ...s2_auth_executes_3DS2_0_authentication.yml | 33 +++++++ ...sult_gets_3DS2_0_authentication_result.yml | 33 +++++++ spec/gmo/shop_api_spec.rb | 90 +++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_exec_tran_with_3DS2_0_returns_3DS2_0_challenge_flow_response.yml create mode 100644 fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_secure_tran_2_completes_payment_after_3DS2_0_challenge_verification.yml create mode 100644 fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_tds2_auth_executes_3DS2_0_authentication.yml create mode 100644 fixtures/vcr_cassettes/GMO_Payment_ShopAPI/_tds2_result_gets_3DS2_0_authentication_result.yml 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/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