diff options
| -rw-r--r-- | client.go | 65 | ||||
| -rw-r--r-- | client_options.go | 32 | ||||
| -rw-r--r-- | client_test.go | 33 | ||||
| -rw-r--r-- | duitku.go | 2 | ||||
| -rw-r--r-- | request.go | 16 | ||||
| -rw-r--r-- | rtol_inquiry.go | 46 | ||||
| -rw-r--r-- | rtol_inquiry_response.go | 30 | ||||
| -rw-r--r-- | testdata/disbursement_rtol_inquiry_test.txt | 16 |
8 files changed, 236 insertions, 4 deletions
@@ -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 @@ -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` ) @@ -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" +} |
