Skip to content

Commit

Permalink
create payment: prevent unmarshalling errors for pay_amount (#37)
Browse files Browse the repository at this point in the history
Fixes #36
  • Loading branch information
Mathias M authored Jan 24, 2023
1 parent 070eded commit e6347ed
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 82 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
v 1.0.4
- Remove hacks for pay_amount and payment_id with custom unmarshalling. #36

v 1.0.3
- Unmarshal error on payment creation in production environment. #34

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Topic|Endpoint|Package.Method|Implemented
## Installation

```bash
$ go get github.com/matm/[email protected].3
$ go get github.com/matm/[email protected].4
```

## CLI Tool
Expand Down
14 changes: 3 additions & 11 deletions payments/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ import (
"github.com/rotisserie/eris"
)

// PaymentHack has all the fields from Payment except the ID field which has
// a different type due to an API inconsistency on their side.
type PaymentHack struct {
Payment
// Using ID as an int instead a string as a quick work around.
ID int `json:"payment_id"`
}

// ListOption are options applying to the list of transactions.
type ListOption struct {
DateFrom string
Expand All @@ -29,7 +21,7 @@ type ListOption struct {

// List returns a list of all transactions for a given API key, depending
// on the supplied options (which can be nil).
func List(o *ListOption) ([]*PaymentHack, error) {
func List(o *ListOption) ([]*Payment, error) {
u := url.Values{}
if o != nil {
if o.Limit != 0 {
Expand All @@ -54,9 +46,9 @@ func List(o *ListOption) ([]*PaymentHack, error) {
return nil, eris.Wrap(err, "list")
}
type plist struct {
Data []*PaymentHack `json:"data"`
Data []*Payment `json:"data"`
}
pl := &plist{Data: make([]*PaymentHack, 0)}
pl := &plist{Data: make([]*Payment, 0)}
par := &core.SendParams{
RouteName: "payments-list",
Into: pl,
Expand Down
33 changes: 28 additions & 5 deletions payments/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"strings"
"testing"

"github.com/matm/go-nowpayments/mocks"
"github.com/matm/go-nowpayments/core"
"github.com/matm/go-nowpayments/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
Expand All @@ -18,7 +18,7 @@ func TestList(t *testing.T) {
name string
o *ListOption
init func(*mocks.HTTPClient)
after func([]*PaymentHack, error)
after func([]*Payment, error)
}{
{"route and response", nil,
func(c *mocks.HTTPClient) {
Expand All @@ -35,9 +35,32 @@ func TestList(t *testing.T) {
return nil
}, nil)
},
func(ps []*PaymentHack, err error) {
func(ps []*Payment, err error) {
assert.NoError(err)
if assert.Len(ps, 1) {
assert.Equal("1", ps[0].ID)
}
}},
{"payment_id as a string", nil,
func(c *mocks.HTTPClient) {
c.EXPECT().Do(mock.Anything).Call.Return(
func(req *http.Request) *http.Response {
switch req.URL.Path {
case "/v1/auth":
return newResponseOK(`{"token":"tok"}`)
case "/v1/payment/":
return newResponseOK(`{"data":[{"payment_id":"54321"}]}`)
default:
t.Fatalf("unexpected route call %q", req.URL.Path)
}
return nil
}, nil)
},
func(ps []*Payment, err error) {
assert.NoError(err)
assert.Len(ps, 1)
if assert.Len(ps, 1) {
assert.Equal("54321", ps[0].ID)
}
}},
{"api error", nil,
func(c *mocks.HTTPClient) {
Expand All @@ -62,7 +85,7 @@ func TestList(t *testing.T) {
func(c *mocks.HTTPClient) {
c.EXPECT().Do(mock.Anything).Return(nil, errors.New("bad credentials"))
},
func(ps []*PaymentHack, err error) {
func(ps []*Payment, err error) {
assert.Nil(ps)
assert.Error(err)
assert.Equal("list: auth: bad credentials", err.Error())
Expand Down
121 changes: 75 additions & 46 deletions payments/payment.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package payments
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"

"github.com/matm/go-nowpayments/config"
"github.com/matm/go-nowpayments/core"
"github.com/rotisserie/eris"
)
Expand Down Expand Up @@ -77,12 +77,78 @@ type Payment struct {
UpdatedAt string `json:"updated_at"`
}

// PaymentProd is an ugly hack. This is because the production env returns a string for `pay_amount`
// whereas the sandbox env returns a float64 :(
// Hopefully they will fix this soon.
type PaymentProd struct {
Payment
PayAmount string `json:"pay_amount"`
// UnmarshalJSON provides custom unmarshalling to the Payment struct so it
// can work it all known cases.
// This is to prevent 2 inconsistencies where their API returns:
// ID as an int (after "list payments" call) or a string (after "create payment" call)
// PayAmount as a string or a float64 (difference betwwen prod and sandbox APIs).
func (p *Payment) UnmarshalJSON(b []byte) error {
type sp struct {
PaymentAmount

ID interface{} `json:"payment_id"`
AmountReceived float64 `json:"amount_received"`
BurningPercent int `json:"burning_percent"`
CreatedAt string `json:"created_at"`
ExpirationEstimateDate string `json:"expiration_estimate_date"`
Network string `json:"network"`
NetworkPrecision int `json:"network_precision"`
PayAddress string `json:"pay_address"`
PayAmount interface{} `json:"pay_amount"`
PayCurrency string `json:"pay_currency"`
PayinExtraID string `json:"payin_extra_id"`
PurchaseID string `json:"purchase_id"`
SmartContract string `json:"smart_contract"`
Status string `json:"payment_status"`
TimeLimit string `json:"time_limit"`
UpdatedAt string `json:"updated_at"`
}
j := sp{}
err := json.Unmarshal(b, &j)
if err != nil {
return eris.Wrap(err, "payment custom unmarshal")
}
z := Payment{
PaymentAmount: j.PaymentAmount,
AmountReceived: j.AmountReceived,
BurningPercent: j.BurningPercent,
CreatedAt: j.CreatedAt,
ExpirationEstimateDate: j.ExpirationEstimateDate,
Network: j.Network,
NetworkPrecision: j.NetworkPrecision,
PayAddress: j.PayAddress,
PayCurrency: j.PayCurrency,
PayinExtraID: j.PayinExtraID,
PurchaseID: j.PurchaseID,
SmartContract: j.SmartContract,
Status: j.Status,
TimeLimit: j.TimeLimit,
UpdatedAt: j.UpdatedAt,
}
switch j.PayAmount.(type) {
case string:
pa, err := strconv.ParseFloat(j.PayAmount.(string), 64)
if err != nil {
return eris.Wrap(err, "parsing pay_amount as a float")
}
z.PayAmount = pa
case float64:
z.PayAmount = j.PayAmount.(float64)
default:
// Any other type (including nil) converts to a zero value,
// which is the default. Do nothing.
}
switch j.ID.(type) {
case string:
z.ID = j.ID.(string)
case float64:
z.ID = fmt.Sprintf("%d", int(j.ID.(float64)))
default:
// Any other type converts to the default value for the type.
// Do nothing.
}
*p = z
return nil
}

// New creates a payment.
Expand All @@ -94,13 +160,7 @@ func New(pa *PaymentArgs) (*Payment, error) {
if err != nil {
return nil, eris.Wrap(err, "payment args")
}
var p interface{}
// Ugly hack but required at the moment :(
if config.Server() == string(core.ProductionBaseURL) {
p = &PaymentProd{}
} else {
p = &Payment{}
}
p := &Payment{}
par := &core.SendParams{
RouteName: "payment-create",
Into: &p,
Expand All @@ -110,38 +170,7 @@ func New(pa *PaymentArgs) (*Payment, error) {
if err != nil {
return nil, err
}
// Ugly hack continuing ...
var pv *Payment
switch p.(type) {
case *Payment:
pv = p.(*Payment)
case *PaymentProd:
j := p.(*PaymentProd)
pv = &Payment{
ID: j.ID,
AmountReceived: j.AmountReceived,
BurningPercent: j.BurningPercent,
CreatedAt: j.CreatedAt,
ExpirationEstimateDate: j.ExpirationEstimateDate,
Network: j.Network,
NetworkPrecision: j.NetworkPrecision,
PayAddress: j.PayAddress,
PayCurrency: j.PayCurrency,
PayinExtraID: j.PayinExtraID,
PurchaseID: j.PurchaseID,
SmartContract: j.SmartContract,
Status: j.Status,
TimeLimit: j.TimeLimit,
UpdatedAt: j.UpdatedAt,
}
// Now convert the `pay_amount`.
pm, err := strconv.ParseFloat(j.PayAmount, 64)
if err != nil {
return nil, eris.Wrap(err, "pay_amount hack convert")
}
pv.PayAmount = pm
}
return pv, nil
return p, nil
}

type InvoicePaymentArgs struct {
Expand Down
48 changes: 29 additions & 19 deletions payments/payment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@ package payments
import (
"errors"
"net/http"
"strings"
"testing"

"github.com/matm/go-nowpayments/config"
"github.com/matm/go-nowpayments/core"
"github.com/matm/go-nowpayments/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestNew(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
tests := []struct {
name string
pa *PaymentArgs
Expand Down Expand Up @@ -49,43 +45,57 @@ func TestNew(t *testing.T) {
assert.NoError(err)
assert.NotNil(p)
assert.Equal("1234", p.ID)
t.Logf("%+v", p)
},
},
{"hack check", &PaymentArgs{
{"pay_amount as a string", &PaymentArgs{
PurchaseID: "1234",
PaymentAmount: PaymentAmount{PriceAmount: 10.0},
},
func(c *mocks.HTTPClient) {
resp := newResponseOK(`{"payment_id":"1234","pay_amount":"3.5"}`)
c.EXPECT().Do(mock.Anything).Return(resp, nil)
// Forces the detection of the production environment.
err := config.Load(strings.NewReader(`{"server":"https://api.nowpayments.io/v1","apiKey":"a","login":"l","password":"p"}`))
require.NoError(err)
}, func(p *Payment, err error) {
assert.NoError(err)
assert.NotNil(p)
assert.Equal("1234", p.ID)
assert.Equal(3.5, p.PayAmount)
// Restore env.
err = config.Load(strings.NewReader(`{"server":"https://api-sandbox.nowpayments.io/v1","apiKey":"a","login":"l","password":"p"}`))
require.NoError(err)
},
},
{"hack check, empty pay amount", &PaymentArgs{
{"pay_amount as a float", &PaymentArgs{
PurchaseID: "1234",
PaymentAmount: PaymentAmount{PriceAmount: 10.0},
},
func(c *mocks.HTTPClient) {
resp := newResponseOK(`{"payment_id":"1234","pay_amount":4.2}`)
c.EXPECT().Do(mock.Anything).Return(resp, nil)
}, func(p *Payment, err error) {
assert.NoError(err)
assert.NotNil(p)
assert.Equal("1234", p.ID)
assert.Equal(4.2, p.PayAmount)
},
},
{"pay_amount as an integer, who knows...", &PaymentArgs{
PurchaseID: "1234",
PaymentAmount: PaymentAmount{PriceAmount: 10.0},
},
func(c *mocks.HTTPClient) {
resp := newResponseOK(`{"payment_id":"1234","pay_amount":100}`)
c.EXPECT().Do(mock.Anything).Return(resp, nil)
}, func(p *Payment, err error) {
assert.NoError(err)
},
},
{"missing pay_amount value", &PaymentArgs{
PurchaseID: "1234",
PaymentAmount: PaymentAmount{PriceAmount: 10.0},
},
func(c *mocks.HTTPClient) {
resp := newResponseOK(`{"payment_id":"1234"}`)
c.EXPECT().Do(mock.Anything).Return(resp, nil)
// Forces the detection of the production environment.
err := config.Load(strings.NewReader(`{"server":"https://api.nowpayments.io/v1","apiKey":"a","login":"l","password":"p"}`))
require.NoError(err)
}, func(p *Payment, err error) {
assert.Error(err)
// Restore env.
err = config.Load(strings.NewReader(`{"server":"https://api-sandbox.nowpayments.io/v1","apiKey":"a","login":"l","password":"p"}`))
require.NoError(err)
assert.NoError(err)
},
},
{"route check", &PaymentArgs{},
Expand Down

0 comments on commit e6347ed

Please sign in to comment.