From 0eef440900eda71b3a257707e49e0ad53be5353f Mon Sep 17 00:00:00 2001 From: Shulhan Date: Fri, 4 Nov 2022 13:49:28 +0700 Subject: all: implement API for online transfer inquiry The RtolInquiry method get the information of the name of the account owner of the transfer destination. After getting this information, customers can determine whether the purpose of such a transfer is in accordance with the intended or not. If appropriate, the customer can proceed to the transfer process. Ref: https://docs.duitku.com/disbursement/en/#transfer-online --- client.go | 65 +++++++++++++++++++++++++++++ client_options.go | 32 ++++++++++++++ client_test.go | 33 +++++++++++++++ duitku.go | 2 + request.go | 16 +++++-- rtol_inquiry.go | 46 ++++++++++++++++++++ rtol_inquiry_response.go | 30 +++++++++++++ testdata/disbursement_rtol_inquiry_test.txt | 16 +++++++ 8 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 rtol_inquiry.go create mode 100644 rtol_inquiry_response.go create mode 100644 testdata/disbursement_rtol_inquiry_test.txt diff --git a/client.go b/client.go index 1a531dd..8768a5b 100644 --- a/client.go +++ b/client.go @@ -8,7 +8,9 @@ import ( "fmt" "net/http" "sort" + "strconv" "strings" + "time" libhttp "github.com/shuLhan/share/lib/http" ) @@ -16,6 +18,10 @@ import ( const ( PathDisbursementListBank = `/disbursement/listBank` PathDisbursementCheckBalance = `/disbursement/checkBalance` + + // Path for transfer online. + PathDisbursementInquiry = `/disbursement/inquiry` + PathDisbursementInquirySandbox = `/disbursement/inquirysandbox` // Used when server URL is sandbox (testing). ) type Client struct { @@ -27,11 +33,17 @@ type Client struct { // NewClient create and initialize new Client. func NewClient(opts ClientOptions) (cl *Client, err error) { var ( + logp = `NewClient` httpcOpts = libhttp.ClientOptions{ ServerUrl: opts.ServerUrl, } ) + err = opts.validate() + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + cl = &Client{ Client: libhttp.NewClient(&httpcOpts), opts: opts, @@ -117,3 +129,56 @@ func (cl *Client) DisbursementListBank() (banks []Bank, err error) { return banks, nil } + +// RtolInquiry get the information of the name of the account owner of the +// transfer destination. +// +// After getting this information, customers can determine whether the purpose +// of such a transfer is in accordance with the intended or not. +// If appropriate, the customer can proceed to the transfer process. +// +// Ref: https://docs.duitku.com/disbursement/en/#transfer-online +func (cl *Client) RtolInquiry(req RtolInquiry) (res *RtolInquiryResponse, err error) { + var ( + now = time.Now() + logp = `RtolInquiry` + path = PathDisbursementInquiry + + resHttp *http.Response + resBody []byte + ) + + // Since the path is different in test environment, we check the host + // here to set it. + if cl.opts.host != hostLive { + path = PathDisbursementInquirySandbox + } + + req.UserID, err = strconv.ParseInt(cl.opts.UserID, 10, 64) + if err != nil { + return nil, fmt.Errorf(`%s: %s`, logp, err) + } + + req.Email = cl.opts.Email + req.Timestamp = now.UnixMilli() + + req.sign(cl.opts.ApiKey) + + resHttp, resBody, err = cl.PostJSON(path, nil, req) + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + if resHttp.StatusCode >= 500 { + return nil, fmt.Errorf(`%s: %s`, logp, resHttp.Status) + } + + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + if res.Code != resCodeSuccess { + return nil, fmt.Errorf(`%s: %s: %s`, logp, res.Code, res.Desc) + } + + return res, nil +} diff --git a/client_options.go b/client_options.go index 7f23c24..5ed7d2c 100644 --- a/client_options.go +++ b/client_options.go @@ -3,10 +3,42 @@ package duitku +import ( + "fmt" + "net/url" +) + // ClientOptions configuration for HTTP client. type ClientOptions struct { ServerUrl string UserID string Email string ApiKey string + + // The hostname extracted from ServerUrl. + host string +} + +// validate each field values. +func (opts *ClientOptions) validate() (err error) { + var ( + urlServer *url.URL + ) + + urlServer, err = url.Parse(opts.ServerUrl) + if err != nil { + return fmt.Errorf(`invalid or empty ServerUrl: %s`, opts.ServerUrl) + } + opts.host = urlServer.Host + + if len(opts.UserID) == 0 { + return fmt.Errorf(`invalid or empty UserID: %s`, opts.UserID) + } + if len(opts.Email) == 0 { + return fmt.Errorf(`invalid or empty Email: %s`, opts.Email) + } + if len(opts.ApiKey) == 0 { + return fmt.Errorf(`invalid or empty ApiKey: %s`, opts.ApiKey) + } + return nil } diff --git a/client_test.go b/client_test.go index 95f61a6..09db153 100644 --- a/client_test.go +++ b/client_test.go @@ -44,6 +44,39 @@ func TestClient_DisbursementCheckBalance(t *testing.T) { test.Assert(t, `DisbursementCheckBalance`, string(exp), string(got)) } +func TestClient_RtolInquiry_live(t *testing.T) { + t.Skip(`This test require external call to server`) + + var ( + inquiryReq RtolInquiry + err error + tdata *test.Data + inquiryRes *RtolInquiryResponse + ) + + tdata, err = test.LoadData(`testdata/disbursement_rtol_inquiry_test.txt`) + if err != nil { + t.Fatal(err) + } + + err = json.Unmarshal(tdata.Input[`request.json`], &inquiryReq) + if err != nil { + t.Fatal(err) + } + + inquiryRes, err = testClient.RtolInquiry(inquiryReq) + if err != nil { + t.Fatal(err) + } + + // We cannot compare the response, because for each call to server + // it will return different CustRefNumber and DisburseID. + + t.Logf(`inquiryRes: %+v`, inquiryRes) + + test.Assert(t, `AccountName`, `Test Account`, inquiryRes.AccountName) +} + func TestClient_DisbursementListBank(t *testing.T) { var ( tdata *test.Data diff --git a/duitku.go b/duitku.go index b4d9fd2..9a16fa6 100644 --- a/duitku.go +++ b/duitku.go @@ -9,4 +9,6 @@ package duitku const ( ServerUrlLive = `https://passport.duitku.com/webapi/api` ServerUrlSandbox = `https://sandbox.duitku.com/webapi/api` + + hostLive = `passport.duitku.com` ) diff --git a/request.go b/request.go index 043a414..b446b32 100644 --- a/request.go +++ b/request.go @@ -7,19 +7,27 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "strconv" "time" ) // request define common HTTP request fields. type request struct { - UserID string `json:"userID"` - Email string `json:"email"` + // Merchant email, filled from ClientOptions.Email. + Email string `json:"email"` + + // Hash of some fields in the request along with its ApiKey. Signature string `json:"signature"` - Timestamp int64 `json:"timestamp"` + + // Merchant ID, filled from ClientOptions.UserID. + UserID int64 `json:"userId"` + + // Unix Timestamp in milliseconds. + Timestamp int64 `json:"timestamp"` } func createRequest(opts ClientOptions) (req request) { - req.UserID = opts.UserID + req.UserID, _ = strconv.ParseInt(opts.UserID, 10, 64) req.Email = opts.Email req.Timestamp = time.Now().UnixMilli() diff --git a/rtol_inquiry.go b/rtol_inquiry.go new file mode 100644 index 0000000..33ab55d --- /dev/null +++ b/rtol_inquiry.go @@ -0,0 +1,46 @@ +package duitku + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" +) + +// RtolInquiry contains request to initiate transfer from merchant to +// customer's bank account using [Online Transfer]. +// +// The signature formula is SHA256(email + timestamp + bankCode + +// bankAccount + amountTransfer + purpose + apiKey). +// +// [Online Transfer]: https://docs.duitku.com/disbursement/en/#online-transfer-inquiry-request +type RtolInquiry struct { + // Destination Bank Code. + BankCode string `json:"bankCode"` + + // Destination account number. + BankAccount string `json:"bankAccount"` + + // Description of transfer purpose. + Purpose string `json:"purpose"` + + // Customer name provided by merchant. + SenderName string `json:"senderName"` + + request + + // Customer ID provided by merchant. + SenderID int64 `json:"senderID"` + + // Disbursement transfer amount. + Amount int64 `json:"amountTransfer"` +} + +func (inq *RtolInquiry) sign(apiKey string) { + var ( + plain = fmt.Sprintf(`%s%d%s%s%d%s%s`, inq.Email, + inq.Timestamp, inq.BankCode, inq.BankAccount, + inq.Amount, inq.Purpose, apiKey) + plainHash [sha256.Size]byte = sha256.Sum256([]byte(plain)) + ) + inq.Signature = hex.EncodeToString(plainHash[:]) +} diff --git a/rtol_inquiry_response.go b/rtol_inquiry_response.go new file mode 100644 index 0000000..1edc7a0 --- /dev/null +++ b/rtol_inquiry_response.go @@ -0,0 +1,30 @@ +package duitku + +import "github.com/shuLhan/share/lib/math/big" + +// RtolInquiryResponse contains response from inquiry for Online Transfer. +type RtolInquiryResponse struct { + response + + // Email sent when inquiry process. + Email string `json:"email"` + + // Destination Bank Code. + BankCode string `json:"bankCode"` + + // Destination account number. + BankAccount string `json:"bankAccount"` + + // Disbursement transfer amount. + Amount *big.Rat `json:"amountTransfer"` + + // Bank Account owner. + AccountName string `json:"accountName"` + + // 9 Digit Customer reference number that will be printed when the + // transaction is successful. + CustRefNumber string `json:"custRefNumber"` + + // Disbursement ID from duitku. Please save it for checking purpose. + DisburseID int64 `json:"disburseId"` +} diff --git a/testdata/disbursement_rtol_inquiry_test.txt b/testdata/disbursement_rtol_inquiry_test.txt new file mode 100644 index 0000000..b72eab9 --- /dev/null +++ b/testdata/disbursement_rtol_inquiry_test.txt @@ -0,0 +1,16 @@ +Test disbursement transfer online inquiry. + +Ref: https://docs.duitku.com/disbursement/en/#transfer-online + +>>> request.json +{ + "userId": 3551, + "email": "test@chakratechnology.com", + "amountTransfer": 10000, + "bankAccount": "8760673566", + "bankCode": "002", + "purpose": "Test Transfer Online Inquiry with duitku.", + "timestamp": 1506486841000, + "senderId": 123456789, + "senderName": "John Doe" +} -- cgit v1.3