From a5b22d2d0d018d7822a492979f36470e9f9cc78c Mon Sep 17 00:00:00 2001 From: Shulhan Date: Tue, 31 Jan 2023 21:24:56 +0700 Subject: all: implement API for Merchant Inquiry The MerchantInquiry API request payment to the Duitku system (via virtual account numbers, QRIS, e-wallet, and so on). Ref: https://docs.duitku.com/api/en/#request-transaction --- account_link.go | 47 +++++++++++++++ address.go | 17 ++++++ client.go | 44 ++++++++++++-- client_test.go | 57 ++++++++++++++++++ customer_detail.go | 16 +++++ duitku.go | 3 + item_detail.go | 18 ++++++ merchant_inquiry.go | 117 +++++++++++++++++++++++++++++++++++++ merchant_inquiry_response.go | 28 +++++++++ testdata/merchant/inquiry_test.txt | 21 +++++++ 10 files changed, 363 insertions(+), 5 deletions(-) create mode 100644 account_link.go create mode 100644 address.go create mode 100644 customer_detail.go create mode 100644 item_detail.go create mode 100644 merchant_inquiry.go create mode 100644 merchant_inquiry_response.go create mode 100644 testdata/merchant/inquiry_test.txt diff --git a/account_link.go b/account_link.go new file mode 100644 index 0000000..b5b8ed4 --- /dev/null +++ b/account_link.go @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package duitku + +// [AccountLink] Parameter for payment methods that use OVO Account Link and +// Shopee Account Link. +// +// [AccountLink]: https://docs.duitku.com/api/en/#account-link +type AccountLink struct { + // [REQ] Credential Code provide by Duitku. + CredentialCode string `json:"credentialCode"` + + // [REQ] Mandatory for OVO payment. + OVO AccountLinkOvo `json:"ovo"` + + // [REQ] Mandatory for Shopee payment. + Shopee AccountLinkShopee `json:"shopee"` +} + +type AccountLinkOvo struct { + PaymentDetails []OvoPaymentDetail `json:"paymentDetails"` +} + +// [AccountLinkOvo] payment detail with OVO. +// +// [AccountLinkOvo]: https://docs.duitku.com/api/en/#ovo-detail +type OvoPaymentDetail struct { + // [REQ] Type of your payment. + PaymentType string `json:"paymentType"` + + // [REQ] Transaction payment amount. + Amount int64 `json:"amount"` +} + +// [AccountLinkShopee] payment detail with Shopee. +// +// [AccountLinkShopee]: https://docs.duitku.com/api/en/#shopee-detail +type AccountLinkShopee struct { + // [REQ] Voucher code. + PromoIDs string `json:"promo_ids"` + + // [REQ] Used for shopee coin from linked ShopeePay account. + // Set true when pay transaction would like to use coins (Only for + // ShopeePay account link). + UseCoin bool `json:"useCoin"` +} diff --git a/address.go b/address.go new file mode 100644 index 0000000..825dab5 --- /dev/null +++ b/address.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package duitku + +// [Address] contains detailed address of customer. +// +// [Address]: https://docs.duitku.com/api/en/#address +type Address struct { + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Address string `json:"address"` + City string `json:"city"` + PostalCode string `json:"postalCode"` + Phone string `json:"phone"` + CountryCode string `json:"countryCode"` +} diff --git a/client.go b/client.go index b831c06..4d4ceca 100644 --- a/client.go +++ b/client.go @@ -37,6 +37,7 @@ const ( PathTransferClearingSandbox = `/webapi/api/disbursement/transferclearingsandbox` // Used for testing. PathMerchantPaymentMethod = `/webapi/api/merchant/paymentmethod/getpaymentmethod` + PathMerchantInquiry = `/webapi/api/merchant/v2/inquiry` ) type Client struct { @@ -54,7 +55,7 @@ func NewClient(opts ClientOptions) (cl *Client, err error) { } ) - err = opts.validate() + err = opts.initAndValidate() if err != nil { return nil, fmt.Errorf(`%s: %w`, logp, err) } @@ -258,6 +259,39 @@ func (cl *Client) ListBank() (banks []Bank, err error) { return banks, nil } +// MerchantInquiry request payment to the Duitku system (via virtual account +// numbers, QRIS, e-wallet, and so on). +// +// Ref: https://docs.duitku.com/api/en/#request-transaction +func (cl *Client) MerchantInquiry(req MerchantInquiry) (resp *MerchantInquiryResponse, err error) { + var ( + logp = `MerchantInquiry` + inreq = merchantInquiry{ + MerchantInquiry: req, + } + + httpRes *http.Response + resBody []byte + ) + + inreq.sign(cl.opts) + + httpRes, resBody, err = cl.PostJSON(PathMerchantInquiry, nil, inreq) + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + if httpRes.StatusCode >= 400 { + return nil, fmt.Errorf(`%s: %s: %s`, logp, httpRes.Status, resBody) + } + + err = json.Unmarshal(resBody, &resp) + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + return resp, nil +} + // MerchantPaymentMethod get active payment methods from the merchant (your) // project. // @@ -267,18 +301,18 @@ func (cl *Client) MerchantPaymentMethod(req *PaymentMethod) (resp *PaymentMethod logp = `MerchantPaymentMethod` path = PathMerchantPaymentMethod - resHttp *http.Response + httpRes *http.Response resBody []byte ) req.Sign(cl.opts) - resHttp, resBody, err = cl.PostJSON(path, nil, req) + httpRes, 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: %w`, logp, err) + if httpRes.StatusCode >= 400 { + return nil, fmt.Errorf(`%s: %s: %s`, logp, httpRes.Status, resBody) } err = json.Unmarshal(resBody, &resp) diff --git a/client_test.go b/client_test.go index a4c9a1e..832bf3a 100644 --- a/client_test.go +++ b/client_test.go @@ -4,6 +4,7 @@ package duitku import ( + "bytes" "encoding/json" "testing" @@ -199,6 +200,62 @@ func TestClient_InquiryStatus_sandbox(t *testing.T) { test.Assert(t, `InquiryStatus`, expInquiryStatus, resInqueryStatus) } +func TestClient_MerchantInquiry(t *testing.T) { + var ( + tdata *test.Data + err error + ) + + err = initClientMerchant() + if err != nil { + t.Skip(err) + } + + tdata, err = test.LoadData(`testdata/merchant/inquiry_test.txt`) + if err != nil { + t.Fatal(err) + } + + var ( + req *MerchantInquiry + resp *MerchantInquiryResponse + tag string + ) + + tag = `request.json` + err = json.Unmarshal(tdata.Input[tag], &req) + if err != nil { + t.Fatal(err) + } + + resp, err = testClientMerchant.MerchantInquiry(*req) + if err != nil { + t.Fatal(err) + } + + var ( + exp []byte + got []byte + ) + + resp.MerchantCode = `[redacted]` + + got, err = json.MarshalIndent(resp, ``, ` `) + if err != nil { + t.Fatal(err) + } + + tag = `response.json` + exp = tdata.Output[tag] + exp = bytes.ReplaceAll(exp, []byte(`$ref`), []byte(resp.Reference)) + exp = bytes.ReplaceAll(exp, []byte(`$payment_url`), []byte(resp.PaymentUrl)) + exp = bytes.ReplaceAll(exp, []byte(`$va`), []byte(resp.VANumber)) + + t.Logf(`MerchantInquiry: response: %s`, got) + + test.Assert(t, `MerchantInquiry`, string(exp), string(got)) +} + func TestClient_MerchantPaymentMethod(t *testing.T) { var ( tdata *test.Data diff --git a/customer_detail.go b/customer_detail.go new file mode 100644 index 0000000..564651c --- /dev/null +++ b/customer_detail.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package duitku + +// [CustomerDetail] detail of customer information for payment to merchant. +// +// [CustomerDetail]: https://docs.duitku.com/api/en/#customer-detail +type CustomerDetail struct { + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Email string `json:"email"` + PhoneNumber string `json:"phoneNumber"` + BillingAddress Address `json:"billingAddress"` + ShippingAddress Address `json:"shippingAddress"` +} diff --git a/duitku.go b/duitku.go index 439e84e..f95d28e 100644 --- a/duitku.go +++ b/duitku.go @@ -3,6 +3,9 @@ // Package duitku provide library and HTTP client for [duitku.com]. // +// In the types, field that tagged with [REQ] is required and [OPT] is +// optional. +// // [duitku.com]: https://docs.duitku.com/disbursement/id/#langkah-awal package duitku diff --git a/item_detail.go b/item_detail.go new file mode 100644 index 0000000..04a3525 --- /dev/null +++ b/item_detail.go @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package duitku + +// [ItemDetail] define the subset of product being payed during payment. +// +// [ItemDetail]: https://docs.duitku.com/api/en/#item-details +type ItemDetail struct { + // [REQ] Name of the item. + Name string `json:"name"` + + // [REQ] Quantity of the item bought. + Quantity int64 `json:"quantity"` + + // [REQ] Price of the Item. Note: Don't add decimal + Price int64 `json:"price"` +} diff --git a/merchant_inquiry.go b/merchant_inquiry.go new file mode 100644 index 0000000..26b8d35 --- /dev/null +++ b/merchant_inquiry.go @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2023 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package duitku + +import ( + "crypto/md5" + "encoding/hex" + "fmt" +) + +// MerchantInquiry define request data for payment using merchant. +type MerchantInquiry struct { + // [REQ] Transaction number from merchant. + // Every request for a new transaction must use a new ID. + MerchantOrderId string `json:"merchantOrderId"` + + // [REQ] PaymentMethod type of payment. + // + // Ref: https://docs.duitku.com/api/en/#payment-method + PaymentMethod string `json:"paymentMethod"` + + // [REQ] Description about product/service on sale. + // + // You can fill in ProductDetails with a description of the product or + // service that you provide. + // You can also insert your store or brand name for more details. + // Then in ItemDetails you can fill in product variants or product + // model details, and other details about the products/services listed + // in the transaction. + ProductDetails string `json:"productDetails"` + + // [REQ] Customer's email. + Email string `json:"email"` + + // [REQ] The name that would be shown at bank payment system. + CustomerVaName string `json:"customerVaName"` + + // [OPT] Additional parameter to be used by merchant. + // If its set, the value must be URL encoded. + AdditionalParam string `json:"additionalParam,omitempty"` + + // [OPT] Customer's username. + MerchantUserInfo string `json:"merchantUserInfo,omitempty"` + + // [OPT] Customer's phone number. + PhoneNumber string `json:"phoneNumber,omitempty"` + + // [OPT] Customer's details. + // [REQ] If PaymentMethod is Credit (DN/AT). + CustomerDetail *CustomerDetail `json:"customerDetail,omitempty"` + + // [OPT] Details for payment method. + AccountLink *AccountLink `json:"accountLink,omitempty"` + + // [OPT] Detail of product being payed. + // [REQ] If PaymentMethod is Credit (DN/AT). + // + // The total of all price in ItemDetails must exactly match the + // PaymentAmount. + ItemDetails []ItemDetail `json:"itemDetails,omitempty"` + + // [REQ] Amount of transaction. + // + // Make sure the PaymentAmount is equal to the total Price in the + // ItemDetails. + PaymentAmount int64 `json:"paymentAmount"` +} + +// merchantInquiry contains internal fields that will be set by client +// during Sign. +type merchantInquiry struct { + // [REQ] A link for callback transaction. + // Default to ClientOptions.MerchantCallbackUrl. + CallbackUrl string `json:"callbackUrl"` + + // [REQ] MerchantCode is a project that use Duitku. + // + // You can get this code on every project you register on the + // [merchant portal]. + // Default to ClientOptions.MerchantCode. + // + // [merchant portal]: https://passport.duitku.com/merchant/Project + MerchantCode string `json:"merchantCode"` + + // [REQ] A link that is used for redirect after exit payment page, + // being paid or not. + // Default to ClientOptions.MerchantReturnUrl. + ReturnUrl string `json:"returnUrl"` + + // [REQ] Transaction security identification code. + // Formula: MD5(merchantCode + merchantOrderId + paymentAmount + apiKey). + Signature string `json:"signature"` + + MerchantInquiry + + // [OPT] Transaction expiry period in minutes. + // If its empty, it will set to [default] based on PaymentMethod. + // + // [default]: https://docs.duitku.com/api/en/#expiry-period + ExpiryPeriod int `json:"expiryPeriod,omitempty"` +} + +func (inq *merchantInquiry) sign(opts ClientOptions) { + var merchant = opts.Merchant(inq.PaymentMethod) + + inq.CallbackUrl = merchant.CallbackUrl + inq.MerchantCode = merchant.Code + inq.ReturnUrl = merchant.ReturnUrl + + var ( + plain = fmt.Sprintf(`%s%s%d%s`, inq.MerchantCode, inq.MerchantOrderId, inq.PaymentAmount, merchant.ApiKey) + plainmd5 = md5.Sum([]byte(plain)) + ) + + inq.Signature = hex.EncodeToString(plainmd5[:]) +} diff --git a/merchant_inquiry_response.go b/merchant_inquiry_response.go new file mode 100644 index 0000000..61f9f19 --- /dev/null +++ b/merchant_inquiry_response.go @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package duitku + +// MerchantInquiryResponse contains response from MerchantInquiry. +type MerchantInquiryResponse struct { + Response + + // Indicates which project used in this transaction. + MerchantCode string `json:"merchantCode"` + + // Reference number from Duitku (need to be save on your system). + Reference string `json:"reference"` + + // Payment link for direction to Duitku payment page. + PaymentUrl string `json:"paymentUrl"` + + // Payment number or virtual account. + VANumber string `json:"vaNumber"` + + // QR string is used if you use QRIS payment (you need to generate QR + // code from this string). + QRString string `json:"qrString"` + + // Payment amount. + Amount string `json:"amount"` +} diff --git a/testdata/merchant/inquiry_test.txt b/testdata/merchant/inquiry_test.txt new file mode 100644 index 0000000..c597b22 --- /dev/null +++ b/testdata/merchant/inquiry_test.txt @@ -0,0 +1,21 @@ +>>> request.json +{ + "merchantOrderId": "1", + "paymentMethod": "BT", + "productDetails": "Payment example using VA Bank Permata", + "email": "test@example.com", + "customerVaName": "John Doe", + "paymentAmount": 10000 +} + +<<< response.json +{ + "responseCode": "", + "responseDesc": "", + "merchantCode": "[redacted]", + "reference": "$ref", + "paymentUrl": "$payment_url", + "vaNumber": "$va", + "qrString": "", + "amount": "10000" +} -- cgit v1.3