Skip to content

Commit

Permalink
Invoice (#23)
Browse files Browse the repository at this point in the history
* add implementation, unit tests and example

* adjust mock

* add integration tests that require refactoring after implementing the preapproval_plan client

* Update examples/apis/invoice/get/main.go

Co-authored-by: gdeandradero <[email protected]>

* Update examples/apis/invoice/search/main.go

Co-authored-by: gdeandradero <[email protected]>

* adjustments requested in the review

* Update pkg/invoice/search_request.go

Co-authored-by: gdeandradero <[email protected]>

* adjustments requested - organizing types

* add unit test to search_request file

* adjusts requested to maintain standard

* test adjustment so ID is received by parameter

* adjust searchrequest test

* adjust struct name - PagingResponse

* remove return

---------

Co-authored-by: gdeandradero <[email protected]>
  • Loading branch information
brunacamposxx and gdeandradero authored Mar 6, 2024
1 parent fd76586 commit 17c7b45
Show file tree
Hide file tree
Showing 10 changed files with 549 additions and 0 deletions.
28 changes: 28 additions & 0 deletions examples/apis/invoice/get/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"context"
"fmt"
"github.com/mercadopago/sdk-go/pkg/config"
"github.com/mercadopago/sdk-go/pkg/invoice"
)

func main() {
cfg, err := config.New("{{ACCESS_TOKEN}}")
if err != nil {
fmt.Println(err)
return
}

client := invoice.NewClient(cfg)

invoiceID := "123"

result, err := client.Get(context.Background(), invoiceID)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(result)
}
36 changes: 36 additions & 0 deletions examples/apis/invoice/search/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"context"
"fmt"
"github.com/mercadopago/sdk-go/pkg/config"
"github.com/mercadopago/sdk-go/pkg/invoice"
)

func main() {
cfg, err := config.New("{{ACCESS_TOKEN}}")
if err != nil {
fmt.Println(err)
return
}

client := invoice.NewClient(cfg)

req := invoice.SearchRequest{
Limit: "10",
Offset: "10",
Filters: map[string]string{
"preapproval_id": "preapproval_id",
},
}

result, err := client.Search(context.Background(), req)
if err != nil {
fmt.Println(err)
return
}

for _, inv := range result.Results {
fmt.Println(inv)
}
}
70 changes: 70 additions & 0 deletions pkg/invoice/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package invoice

import (
"context"
"fmt"
"github.com/mercadopago/sdk-go/pkg/config"
"github.com/mercadopago/sdk-go/pkg/internal/baseclient"
"net/url"
)

const (
urlBase = "https://api.mercadopago.com/authorized_payments"
urlSearch = urlBase + "/search"
urlWithID = urlBase + "/:id"
)

// Client contains the methods to interact with the Invoice API.
type Client interface {
// Get finds an invoice by ID.
// It is a get request to the endpoint: https://api.mercadopago.com/authorized_payments/{id}
// Reference: https://www.mercadopago.com/developers/en/reference/subscriptions/_authorized_payments_id/get
Get(ctx context.Context, id string) (*Response, error)

// Search the invoices for a subscriptions by different parameters.
// It is a get request to the endpoint: https://api.mercadopago.com/authorized_payments/search
// Reference: https://www.mercadopago.com/developers/en/reference/subscriptions/_authorized_payments_search/get
Search(ctx context.Context, request SearchRequest) (*SearchResponse, error)
}

// client is the implementation of Client.
type client struct {
cfg *config.Config
}

// NewClient returns a new Invoice API Client.
func NewClient(c *config.Config) Client {
return &client{
cfg: c,
}
}

func (c *client) Get(ctx context.Context, id string) (*Response, error) {
params := map[string]string{
"id": id,
}

res, err := baseclient.Get[*Response](ctx, c.cfg, urlWithID, baseclient.WithPathParams(params))
if err != nil {
return nil, err
}

return res, nil
}

func (c *client) Search(ctx context.Context, request SearchRequest) (*SearchResponse, error) {
params := request.Parameters()

parsedURL, err := url.Parse(urlSearch)
if err != nil {
return nil, fmt.Errorf("error parsing parseUrl: %w", err)
}
parsedURL.RawQuery = params

res, err := baseclient.Get[*SearchResponse](ctx, c.cfg, parsedURL.String())
if err != nil {
return nil, err
}

return res, nil
}
228 changes: 228 additions & 0 deletions pkg/invoice/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package invoice

import (
"context"
"fmt"
"github.com/mercadopago/sdk-go/pkg/config"
"github.com/mercadopago/sdk-go/pkg/internal/httpclient"
"io"
"net/http"
"os"
"reflect"
"strings"
"testing"
"time"
)

var (
getResponseJSON, _ = os.Open("../../resources/mocks/invoice/get_response.json")
getResponse, _ = io.ReadAll(getResponseJSON)
searchResponseJSON, _ = os.Open("../../resources/mocks/invoice/search_response.json")
searchResponse, _ = io.ReadAll(searchResponseJSON)
)

func TestGet(t *testing.T) {
type fields struct {
config *config.Config
}
type args struct {
ctx context.Context
id string
}
tests := []struct {
name string
fields fields
args args
want *Response
wantErr string
}{
{
name: "should_return_error_when_send_request",
fields: fields{
config: &config.Config{
Requester: &httpclient.Mock{
DoMock: func(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("some error")
},
},
},
},
args: args{
ctx: context.Background(),
},
want: nil,
wantErr: "transport level error: some error",
},
{
name: "should_return_response",
fields: fields{
config: &config.Config{
Requester: &httpclient.Mock{
DoMock: func(req *http.Request) (*http.Response, error) {
stringReader := strings.NewReader(string(getResponse))
stringReadCloser := io.NopCloser(stringReader)
return &http.Response{
Body: stringReadCloser,
}, nil
},
},
},
},
args: args{
ctx: context.Background(),
id: "3950169598",
},
want: &Response{
PreapprovalID: "202caa5d4084417b8e2a394121bf172b",
ID: 3950169598,
Type: "recurring",
Status: "processed",
DateCreated: parseDate("2024-02-27T17:42:04.835-04:00"),
LastModified: parseDate("2024-02-27T17:45:06.462-04:00"),
TransactionAmount: 5.00,
CurrencyID: "BRL",
Reason: "Yoga classes",
Payment: PaymentResponse{
ID: 3950169598,
Status: "approved",
StatusDetail: "accredited",
},
RetryAttempt: 1,
NextRetryDate: parseDate("2024-02-28T17:40:33.000-04:00"),
DebitDate: parseDate("2024-02-27T17:40:32.000-04:00"),
PaymentMethodID: "account_money",
},
wantErr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &client{
cfg: tt.fields.config,
}
got, err := c.Get(tt.args.ctx, tt.args.id)
gotErr := ""
if err != nil {
gotErr = err.Error()
}

if gotErr != tt.wantErr {
t.Errorf("client.Get() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("client.Get() = %v, want %v", got, tt.want)
}
})
}
}

func TestSearch(t *testing.T) {
type fields struct {
config *config.Config
}
type args struct {
ctx context.Context
request SearchRequest
}
tests := []struct {
name string
fields fields
args args
want *SearchResponse
wantErr string
}{
{
name: "should_return_error_when_send_request",
fields: fields{
config: &config.Config{
Requester: &httpclient.Mock{
DoMock: func(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("some error")
},
},
},
},
args: args{
ctx: context.Background(),
},
want: nil,
wantErr: "transport level error: some error",
},
{
name: "should_return_response",
fields: fields{
config: &config.Config{
Requester: &httpclient.Mock{
DoMock: func(req *http.Request) (*http.Response, error) {
stringReader := strings.NewReader(string(searchResponse))
stringReadCloser := io.NopCloser(stringReader)
return &http.Response{
Body: stringReadCloser,
}, nil
},
},
},
},
args: args{
ctx: context.Background(),
request: SearchRequest{
Limit: "12",
},
},
want: &SearchResponse{
Results: []Response{
{
PreapprovalID: "202caa5d4084417b8e2a394121bf172b",
ID: 3950169598,
Type: "recurring",
Status: "processed",
DateCreated: parseDate("2024-02-27T17:42:04.835-04:00"),
LastModified: parseDate("2024-02-27T17:45:06.462-04:00"),
TransactionAmount: 5.00,
CurrencyID: "BRL",
Reason: "Yoga classes",
Payment: PaymentResponse{
ID: 3950169598,
Status: "approved",
StatusDetail: "accredited",
},
RetryAttempt: 1,
NextRetryDate: parseDate("2024-02-28T17:40:33.000-04:00"),
DebitDate: parseDate("2024-02-27T17:40:32.000-04:00"),
PaymentMethodID: "account_money",
},
},
Paging: PagingResponse{
Offset: 0,
Limit: 12,
Total: 1,
},
},
wantErr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &client{
cfg: tt.fields.config,
}
got, err := c.Search(tt.args.ctx, tt.args.request)
gotErr := ""
if err != nil {
gotErr = err.Error()
}

if gotErr != tt.wantErr {
t.Errorf("client.Search() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("client.Search() = %v, want %v", got, tt.want)
}
})
}
}

func parseDate(s string) *time.Time {
d, _ := time.Parse(time.RFC3339, s)
return &d
}
31 changes: 31 additions & 0 deletions pkg/invoice/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package invoice

import "time"

// Response is the response from the Invoice API.
type Response struct {
DateCreated *time.Time `json:"date_created"`
DebitDate *time.Time `json:"debit_date"`
LastModified *time.Time `json:"last_modified"`
NextRetryDate *time.Time `json:"next_retry_date"`
Payment PaymentResponse `json:"payment"`

CurrencyID string `json:"currency_id"`
ExternalReference string `json:"external_reference"`
PaymentMethodID string `json:"payment_method_id"`
PreapprovalID string `json:"preapproval_id"`
Reason string `json:"reason"`
Status string `json:"status"`
Summarized string `json:"summarized"`
Type string `json:"type"`
ID int `json:"id"`
RetryAttempt int `json:"retry_attempt"`
TransactionAmount float64 `json:"transaction_amount"`
}

// PaymentResponse contains information about payment.
type PaymentResponse struct {
Status string `json:"status"`
StatusDetail string `json:"status_detail"`
ID int `json:"id"`
}
Loading

0 comments on commit 17c7b45

Please sign in to comment.