mirror of
https://github.com/bestbytes/datatrans.git
synced 2025-10-16 12:05:36 +00:00
add more endpoints
This commit is contained in:
parent
bc81e833f0
commit
07189b5892
228
client.go
228
client.go
@ -2,31 +2,52 @@ package datatrans
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// https://docs.datatrans.ch/docs/api-endpoints
|
||||
const (
|
||||
pathBase = "/v1/transactions"
|
||||
pathStatus = pathBase + "/%s"
|
||||
pathCredit = pathBase + "/%s/credit"
|
||||
pathCreditAuthorize = pathBase + "/credit"
|
||||
pathCancel = pathBase + "/%s/cancel"
|
||||
pathSettle = pathBase + "/%s/settle"
|
||||
pathValidate = pathBase + "/validate"
|
||||
pathAuthorizeTransaction = pathBase + "/%s/authorize"
|
||||
pathAuthorize = pathBase + "/authorize"
|
||||
pathInitialize = pathBase
|
||||
endpointURLSandBox = `https://api.sandbox.datatrans.com`
|
||||
endpointURLProduction = `https://api.datatrans.com`
|
||||
|
||||
pathBase = "/v1/transactions"
|
||||
pathStatus = pathBase + "/%s"
|
||||
pathCredit = pathBase + "/%s/credit"
|
||||
pathCreditAuthorize = pathBase + "/credit"
|
||||
pathCancel = pathBase + "/%s/cancel"
|
||||
pathSettle = pathBase + "/%s/settle"
|
||||
pathValidate = pathBase + "/validate"
|
||||
pathAuthorizeTransaction = pathBase + "/%s/authorize"
|
||||
pathAuthorize = pathBase + "/authorize"
|
||||
pathInitialize = pathBase
|
||||
pathSecureFields = pathBase + "/secureFields"
|
||||
pathSecureFieldsUpdate = pathBase + "/secureFields/%s"
|
||||
pathAliases = pathBase + "/aliases"
|
||||
pathAliasesDelete = pathBase + "/aliases/%s"
|
||||
pathReconciliationsSales = "/v1/reconciliations/sales"
|
||||
pathReconciliationsSalesBulk = "/v1/reconciliations/sales/bulk"
|
||||
)
|
||||
|
||||
type OptionMerchant struct {
|
||||
InternalID string
|
||||
Server string
|
||||
MerchantID string // basic auth user
|
||||
Password string // basic auth pw
|
||||
InternalID string
|
||||
EnableProduction bool
|
||||
// https://docs.datatrans.ch/docs/api-endpoints#section-idempotency
|
||||
// If your request failed to reach our servers, no idempotent result is saved
|
||||
// because no API endpoint processed your request. In such cases, you can
|
||||
// simply retry your operation safely. Idempotency keys remain stored for 3
|
||||
// minutes. After 3 minutes have passed, sending the same request together
|
||||
// with the previous idempotency key will create a new operation.
|
||||
UseIdempotency bool
|
||||
MerchantID string // basic auth user
|
||||
Password string // basic auth pw
|
||||
}
|
||||
|
||||
func (m OptionMerchant) apply(c *Client) error {
|
||||
@ -45,10 +66,9 @@ func (fn OptionHTTPRequestFn) apply(c *Client) error {
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
copyRawResponseBody bool
|
||||
doFn OptionHTTPRequestFn
|
||||
merchants map[string]OptionMerchant // string = your custom merchant ID
|
||||
currentInternalID string
|
||||
doFn OptionHTTPRequestFn
|
||||
merchants map[string]OptionMerchant // string = your custom merchant ID
|
||||
currentInternalID string
|
||||
}
|
||||
|
||||
type Option interface {
|
||||
@ -67,6 +87,16 @@ func MakeClient(opts ...Option) (Client, error) {
|
||||
if len(c.merchants) == 0 {
|
||||
return Client{}, fmt.Errorf("no merchants applied")
|
||||
}
|
||||
if c.doFn == nil {
|
||||
c.doFn = (&http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
},
|
||||
}).Do
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@ -160,19 +190,39 @@ func MarshalJSON(postData interface{}) ([]byte, error) {
|
||||
return jsonBytes, nil
|
||||
}
|
||||
|
||||
func (c *Client) preparePostJSONReq(path string, postData interface{}) (*http.Request, error) {
|
||||
func (c *Client) prepareJSONReq(method, path string, postData interface{}) (*http.Request, error) {
|
||||
internalID := c.currentInternalID
|
||||
|
||||
jsonBytes, err := MarshalJSON(postData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ClientID:%q: failed to json marshal HTTP request: %w", internalID, err)
|
||||
var r io.Reader
|
||||
var jsonBytes []byte
|
||||
if postData != nil {
|
||||
var err error
|
||||
jsonBytes, err = MarshalJSON(postData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ClientID:%q: failed to json marshal HTTP request: %w", internalID, err)
|
||||
}
|
||||
r = bytes.NewReader(jsonBytes)
|
||||
}
|
||||
host := endpointURLSandBox
|
||||
if c.merchants[internalID].EnableProduction {
|
||||
host = endpointURLProduction
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, c.merchants[internalID].Server+path, bytes.NewReader(jsonBytes))
|
||||
req, err := http.NewRequest(method, host+path, r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ClientID:%q: failed to create HTTP request: %w", internalID, err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if postData != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if method == http.MethodPost && c.merchants[internalID].UseIdempotency {
|
||||
// not quite happy with this
|
||||
// https://docs.datatrans.ch/docs/api-endpoints#section-idempotency
|
||||
fh := fnv.New64a()
|
||||
_, _ = fh.Write([]byte(internalID + host + path))
|
||||
_, _ = fh.Write(jsonBytes)
|
||||
req.Header.Set("Idempotency-Key", hex.EncodeToString(fh.Sum(nil)))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
@ -184,7 +234,11 @@ func (c *Client) Status(transactionID string) (*ResponseStatus, error) {
|
||||
return nil, fmt.Errorf("transactionID cannot be empty")
|
||||
}
|
||||
internalID := c.currentInternalID
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(c.merchants[internalID].Server+pathStatus, transactionID), nil)
|
||||
host := endpointURLSandBox
|
||||
if c.merchants[internalID].EnableProduction {
|
||||
host = endpointURLProduction
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(host+pathStatus, transactionID), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ClientID:%q: failed to create HTTP request: %w", internalID, err)
|
||||
}
|
||||
@ -204,7 +258,7 @@ func (c *Client) Credit(transactionID string, rc RequestCredit) (*ResponseCardMa
|
||||
return nil, fmt.Errorf("neither currency nor refno nor transactionID can be empty")
|
||||
}
|
||||
|
||||
req, err := c.preparePostJSONReq(fmt.Sprintf(pathCredit, transactionID), rc)
|
||||
req, err := c.prepareJSONReq(http.MethodPost, fmt.Sprintf(pathCredit, transactionID), rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -225,7 +279,7 @@ func (c *Client) CreditAuthorize(rca RequestCreditAuthorize) (*ResponseCardMaske
|
||||
return nil, fmt.Errorf("neither currency nor refno nor amount can be empty")
|
||||
}
|
||||
|
||||
req, err := c.preparePostJSONReq(pathCreditAuthorize, rca)
|
||||
req, err := c.prepareJSONReq(http.MethodPost, pathCreditAuthorize, rca)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -245,7 +299,7 @@ func (c *Client) Cancel(transactionID string, refno string) error {
|
||||
if transactionID == "" || refno == "" {
|
||||
return fmt.Errorf("neither transactionID nor refno can be empty")
|
||||
}
|
||||
req, err := c.preparePostJSONReq(fmt.Sprintf(pathCancel, transactionID), struct {
|
||||
req, err := c.prepareJSONReq(http.MethodPost, fmt.Sprintf(pathCancel, transactionID), struct {
|
||||
Refno string `json:"refno"`
|
||||
}{
|
||||
Refno: refno,
|
||||
@ -269,7 +323,7 @@ func (c *Client) Settle(transactionID string, rs RequestSettle) error {
|
||||
if transactionID == "" || rs.Amount == 0 || rs.Currency == "" || rs.RefNo == "" {
|
||||
return fmt.Errorf("neither transactionID nor refno nor amount nor currency can be empty")
|
||||
}
|
||||
req, err := c.preparePostJSONReq(fmt.Sprintf(pathSettle, transactionID), rs)
|
||||
req, err := c.prepareJSONReq(http.MethodPost, fmt.Sprintf(pathSettle, transactionID), rs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -289,7 +343,7 @@ func (c *Client) ValidateAlias(rva RequestValidateAlias) (*ResponseCardMasked, e
|
||||
if rva.Currency == "" || rva.RefNo == "" {
|
||||
return nil, fmt.Errorf("neither currency nor refno can be empty")
|
||||
}
|
||||
req, err := c.preparePostJSONReq(pathValidate, rva)
|
||||
req, err := c.prepareJSONReq(http.MethodPost, pathValidate, rva)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -309,7 +363,7 @@ func (c *Client) AuthorizeTransaction(transactionID string, rva RequestAuthorize
|
||||
if transactionID == "" || rva.RefNo == "" {
|
||||
return nil, fmt.Errorf("neither transactionID nor refno can be empty")
|
||||
}
|
||||
req, err := c.preparePostJSONReq(fmt.Sprintf(pathAuthorizeTransaction, transactionID), rva)
|
||||
req, err := c.prepareJSONReq(http.MethodPost, fmt.Sprintf(pathAuthorizeTransaction, transactionID), rva)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -331,7 +385,7 @@ func (c *Client) Authorize(rva RequestAuthorize) (*ResponseCardMasked, error) {
|
||||
if rva.Amount == 0 || rva.Currency == "" || rva.RefNo == "" {
|
||||
return nil, fmt.Errorf("neither transactionID nor amount nor currency nor refno can be empty")
|
||||
}
|
||||
req, err := c.preparePostJSONReq(pathAuthorize, rva)
|
||||
req, err := c.prepareJSONReq(http.MethodPost, pathAuthorize, rva)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -354,9 +408,9 @@ func (c *Client) Authorize(rva RequestAuthorize) (*ResponseCardMasked, error) {
|
||||
// paymentMethod array can be used.
|
||||
func (c *Client) Initialize(rva RequestInitialize) (*ResponseInitialize, error) {
|
||||
if rva.Amount == 0 || rva.Currency == "" || rva.RefNo == "" {
|
||||
return nil, fmt.Errorf("neither transactionID nor amount nor currency nor refno can be empty")
|
||||
return nil, fmt.Errorf("neither amount nor currency nor refno can be empty")
|
||||
}
|
||||
req, err := c.preparePostJSONReq(pathInitialize, rva)
|
||||
req, err := c.prepareJSONReq(http.MethodPost, pathInitialize, rva)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -367,3 +421,109 @@ func (c *Client) Initialize(rva RequestInitialize) (*ResponseInitialize, error)
|
||||
}
|
||||
return &ri, nil
|
||||
}
|
||||
|
||||
// InitializeSecureFields initializes a Secure Fields transaction. Proceed with
|
||||
// the steps below to process Secure Fields payment transactions.
|
||||
// https://api-reference.datatrans.ch/#operation/secureFieldsInit
|
||||
func (c *Client) SecureFieldsInit(rva RequestSecureFieldsInit) (*ResponseInitialize, error) {
|
||||
if rva.Amount == 0 || rva.Currency == "" || rva.ReturnUrl == "" {
|
||||
return nil, fmt.Errorf("neither amount nor currency nor returnURL can be empty")
|
||||
}
|
||||
req, err := c.prepareJSONReq(http.MethodPost, pathSecureFields, rva)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ri ResponseInitialize
|
||||
if err := c.do(req, &ri); err != nil {
|
||||
return nil, fmt.Errorf("ClientID:%q: failed to execute HTTP request: %w", c.currentInternalID, err)
|
||||
}
|
||||
return &ri, nil
|
||||
}
|
||||
|
||||
// SecureFieldsUpdate use this API to update the amount of a Secure Fields
|
||||
// transaction. This action is only allowed before the 3D process. At least one
|
||||
// property must be updated.
|
||||
// https://api-reference.datatrans.ch/#operation/secure-fields-update
|
||||
func (c *Client) SecureFieldsUpdate(transactionID string, rva RequestSecureFieldsUpdate) error {
|
||||
if rva.Amount == 0 || rva.Currency == "" {
|
||||
return fmt.Errorf("neither amount nor currency nor returnURL can be empty")
|
||||
}
|
||||
req, err := c.prepareJSONReq(http.MethodPatch, fmt.Sprintf(pathSecureFieldsUpdate, transactionID), rva)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.do(req, nil); err != nil {
|
||||
return fmt.Errorf("ClientID:%q: failed to execute HTTP request: %w", c.currentInternalID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AliasConvert converts a legacy (numeric or masked) alias to the most recent
|
||||
// alias format.
|
||||
func (c *Client) AliasConvert(legacyAlias string) (string, error) {
|
||||
if legacyAlias == "" {
|
||||
return "", fmt.Errorf("legacyAlias cannot be empty")
|
||||
}
|
||||
req, err := c.prepareJSONReq(http.MethodPost, pathAliases, struct {
|
||||
LegacyAlias string `json:"legacyAlias"`
|
||||
}{
|
||||
LegacyAlias: legacyAlias,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var resp struct {
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
if err := c.do(req, &resp); err != nil {
|
||||
return "", fmt.Errorf("ClientID:%q: failed to execute HTTP request: %w", c.currentInternalID, err)
|
||||
}
|
||||
return resp.Alias, nil
|
||||
}
|
||||
|
||||
// AliasDelete deletes an alias with immediate effect. The alias will no longer
|
||||
// be recognized if used later with any API call.
|
||||
func (c *Client) AliasDelete(alias string) error {
|
||||
if alias == "" {
|
||||
return fmt.Errorf("alias cannot be empty")
|
||||
}
|
||||
req, err := c.prepareJSONReq(http.MethodDelete, fmt.Sprintf(pathAliasesDelete, alias), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.do(req, nil); err != nil {
|
||||
return fmt.Errorf("ClientID:%q: failed to execute HTTP request: %w", c.currentInternalID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReconciliationsSales reports a sale. When using reconciliation, use this API
|
||||
// to report a sale. The matching is based on the transactionId.
|
||||
func (c *Client) ReconciliationsSales(sale RequestReconciliationsSale) (*ResponseReconciliationsSale, error) {
|
||||
req, err := c.prepareJSONReq(http.MethodPost, pathReconciliationsSales, sale)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rrs ResponseReconciliationsSale
|
||||
if err := c.do(req, &rrs); err != nil {
|
||||
return nil, fmt.Errorf("ClientID:%q: failed to execute HTTP request: %w", c.currentInternalID, err)
|
||||
}
|
||||
return &rrs, nil
|
||||
}
|
||||
|
||||
// ReconciliationsSalesBulk reports bulk sales. When using reconciliation, use
|
||||
// this API to report multiples sales with a single API call. The matching is
|
||||
// based on the transactionId.
|
||||
func (c *Client) ReconciliationsSalesBulk(sales RequestReconciliationsSales) (*ResponseReconciliationsSales, error) {
|
||||
req, err := c.prepareJSONReq(http.MethodPost, pathReconciliationsSalesBulk, sales)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rrs ResponseReconciliationsSales
|
||||
if err := c.do(req, &rrs); err != nil {
|
||||
return nil, fmt.Errorf("ClientID:%q: failed to execute HTTP request: %w", c.currentInternalID, err)
|
||||
}
|
||||
return &rrs, nil
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package datatrans_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -50,7 +51,6 @@ func TestClient_Status(t *testing.T) {
|
||||
c, err := datatrans.MakeClient(
|
||||
datatrans.OptionHTTPRequestFn(mockResponse(t, 200, "testdata/status_response.json", nil)),
|
||||
datatrans.OptionMerchant{
|
||||
Server: "http://localhost",
|
||||
MerchantID: "322342",
|
||||
Password: "32168",
|
||||
},
|
||||
@ -73,6 +73,9 @@ func TestClient_Initialize(t *testing.T) {
|
||||
if req.Header.Get("Content-Type") != "application/json" {
|
||||
t.Error("invalid content type")
|
||||
}
|
||||
if k := req.Header.Get("Idempotency-Key"); k != "c0476553a7e7da70" {
|
||||
t.Errorf("invalid Idempotency-Key: %q", k)
|
||||
}
|
||||
|
||||
u, p, _ := req.BasicAuth()
|
||||
if u != "322342" {
|
||||
@ -90,9 +93,9 @@ func TestClient_Initialize(t *testing.T) {
|
||||
}
|
||||
})),
|
||||
datatrans.OptionMerchant{
|
||||
Server: "http://localhost",
|
||||
MerchantID: "322342",
|
||||
Password: "sfdgsdfg",
|
||||
UseIdempotency: true,
|
||||
MerchantID: "322342",
|
||||
Password: "sfdgsdfg",
|
||||
},
|
||||
)
|
||||
must(t, err)
|
||||
@ -141,3 +144,37 @@ func TestMarshalJSON(t *testing.T) {
|
||||
t.Errorf("\nWant: %s\nHave: %s", wantJSON, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_AliasDelete_Error(t *testing.T) {
|
||||
c, err := datatrans.MakeClient(
|
||||
datatrans.OptionHTTPRequestFn(mockResponse(t, 400, `{"error": {"code": "ALIAS_NOT_FOUND"}}`, func(t *testing.T, req *http.Request) {
|
||||
if req.Method != http.MethodDelete {
|
||||
t.Error("not a delete request")
|
||||
}
|
||||
if req.Header.Get("Content-Type") == "application/json" {
|
||||
t.Error("invalid content type")
|
||||
}
|
||||
|
||||
u, p, _ := req.BasicAuth()
|
||||
if u != "322342" {
|
||||
t.Error("invalid basic username")
|
||||
}
|
||||
if p != "sfdgsdfg" {
|
||||
t.Error("invalid basic password")
|
||||
}
|
||||
})),
|
||||
datatrans.OptionMerchant{
|
||||
MerchantID: "322342",
|
||||
Password: "sfdgsdfg",
|
||||
},
|
||||
)
|
||||
must(t, err)
|
||||
|
||||
err = c.AliasDelete("3469efdbbdcb043e56b19ffca69a8be0c5524d89")
|
||||
|
||||
var detailErr datatrans.ErrorResponse
|
||||
errors.As(err, &detailErr)
|
||||
if !reflect.DeepEqual(detailErr, datatrans.ErrorResponse{HTTPStatusCode: 400, ErrorDetail: datatrans.ErrorDetail{Code: "ALIAS_NOT_FOUND", Message: ""}}) {
|
||||
t.Error("errors not equal")
|
||||
}
|
||||
}
|
||||
|
||||
46
dto.go
46
dto.go
@ -28,9 +28,19 @@ func (b *RawJSONBody) setJSONRawBody(p []byte) {
|
||||
*b = p
|
||||
}
|
||||
|
||||
type ResponseAuthorize struct {
|
||||
AcquirerAuthorizationCode string `json:"acquirerAuthorizationCode"`
|
||||
RawJSONBody `json:"raw,omitempty"`
|
||||
// https://api-reference.datatrans.ch/#operation/secureFieldsInit
|
||||
type RequestSecureFieldsInit struct {
|
||||
Currency string `json:"currency"`
|
||||
Amount int `json:"amount,omitempty"`
|
||||
ReturnUrl string `json:"returnUrl"`
|
||||
CustomFields `json:"-"`
|
||||
}
|
||||
|
||||
// https://api-reference.datatrans.ch/#operation/secure-fields-update
|
||||
type RequestSecureFieldsUpdate struct {
|
||||
Currency string `json:"currency"`
|
||||
Amount int `json:"amount,omitempty"`
|
||||
CustomFields `json:"-"`
|
||||
}
|
||||
|
||||
// https://api-reference.datatrans.ch/#operation/init
|
||||
@ -69,6 +79,11 @@ type RequestAuthorize struct {
|
||||
CustomFields `json:"-"`
|
||||
}
|
||||
|
||||
type ResponseAuthorize struct {
|
||||
AcquirerAuthorizationCode string `json:"acquirerAuthorizationCode"`
|
||||
RawJSONBody `json:"raw,omitempty"`
|
||||
}
|
||||
|
||||
type RequestAuthorizeTransaction struct {
|
||||
RefNo string `json:"refno,omitempty"`
|
||||
Amount int `json:"amount,omitempty"`
|
||||
@ -196,6 +211,7 @@ type Customer struct {
|
||||
|
||||
type Theme struct {
|
||||
// Theme configuration options when using the default DT2015 theme
|
||||
Name string `json:"name,omitempty"` // Theme name, e.g. DT2015
|
||||
Configuration ThemeConfiguration `json:"configuration,omitempty"`
|
||||
}
|
||||
|
||||
@ -244,3 +260,27 @@ type InitializeOption struct {
|
||||
RememberMe string `json:"rememberMe"` // Enum: "true" "checked" Whether to show a checkbox on the payment page to let the customer choose if they want to save their card information.
|
||||
ReturnMobileToken bool `json:"returnMobileToken"` // Indicates that a mobile token should be created. This is needed when using our Mobile SDKs.
|
||||
}
|
||||
|
||||
type RequestReconciliationsSale struct {
|
||||
Date time.Time `json:"date"`
|
||||
TransactionID string `json:"transactionId"`
|
||||
Currency string `json:"currency"`
|
||||
Amount int `json:"amount"`
|
||||
Type string `json:"type"`
|
||||
Refno string `json:"refno"`
|
||||
}
|
||||
|
||||
type ResponseReconciliationsSale struct {
|
||||
TransactionID string `json:"transactionId"`
|
||||
SaleDate time.Time `json:"saleDate"`
|
||||
ReportedDate time.Time `json:"reportedDate"`
|
||||
MatchResult string `json:"matchResult"`
|
||||
}
|
||||
|
||||
type RequestReconciliationsSales struct {
|
||||
Sales []RequestReconciliationsSale `json:"sales"`
|
||||
}
|
||||
|
||||
type ResponseReconciliationsSales struct {
|
||||
Sales []ResponseReconciliationsSale `json:"sales"`
|
||||
}
|
||||
|
||||
@ -20,8 +20,8 @@ var (
|
||||
|
||||
// https://api-reference.datatrans.ch/#section/Webhook/Webhook-signing
|
||||
type WebhookOption struct {
|
||||
Sign2HMACKey string
|
||||
ErrorHandler func(error) http.Handler
|
||||
Sign2HMACKey string // hex encoded
|
||||
ErrorHandler func(error) http.Handler // optional custom error handler
|
||||
}
|
||||
|
||||
// ValidateWebhook an HTTP middleware which checks that the signature in the header is valid.
|
||||
@ -55,13 +55,13 @@ func ValidateWebhook(wo WebhookOption) (func(next http.Handler) http.Handler, er
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(io.MultiWriter(&buf, hmv), r.Body); err != nil {
|
||||
_ = r.Body.Close()
|
||||
wo.ErrorHandler(errors.New("ValidateWebhook: copy failed")).ServeHTTP(w, r)
|
||||
wo.ErrorHandler(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
_ = r.Body.Close()
|
||||
r.Body = ioutil.NopCloser(&buf)
|
||||
|
||||
if !hmac.Equal(hmv.Sum(nil), []byte(s0)) {
|
||||
if !hmac.Equal(hmv.Sum(nil), s0) {
|
||||
wo.ErrorHandler(ErrWebhookMismatchSignature).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user