aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2023-01-31 21:24:56 +0700
committerShulhan <ms@kilabit.info>2023-02-01 15:07:22 +0700
commita5b22d2d0d018d7822a492979f36470e9f9cc78c (patch)
tree8050420b39923a9757b347b817c36ba711e0d8d7
parentb4b6699c27b04893c73c50553070682799220080 (diff)
downloadduitku-a5b22d2d0d018d7822a492979f36470e9f9cc78c.tar.xz
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
-rw-r--r--account_link.go47
-rw-r--r--address.go17
-rw-r--r--client.go44
-rw-r--r--client_test.go57
-rw-r--r--customer_detail.go16
-rw-r--r--duitku.go3
-rw-r--r--item_detail.go18
-rw-r--r--merchant_inquiry.go117
-rw-r--r--merchant_inquiry_response.go28
-rw-r--r--testdata/merchant/inquiry_test.txt21
10 files changed, 363 insertions, 5 deletions
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 <ms@kilabit.info>
+// 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 <ms@kilabit.info>
+// 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 <ms@kilabit.info>
+// 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 <ms@kilabit.info>
+// 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 <ms@kilabit.info>
+// 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 <ms@kilabit.info>
+// 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"
+}