diff --git a/examples/apis/customercard/create/main.go b/examples/apis/customercard/create/main.go new file mode 100644 index 00000000..f82906b4 --- /dev/null +++ b/examples/apis/customercard/create/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "fmt" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/customercard" +) + +func main() { + accessToken := "{{ACCESS_TOKEN}}" + + cfg, err := config.New(accessToken) + if err != nil { + fmt.Println(err) + return + } + + req := customercard.Request{Token: "{{CARD_TOKEN}}"} + + client := customercard.NewClient(cfg) + card, err := client.Create(context.Background(), "{{CUSTOMER_ID}}", req) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(card) +} diff --git a/examples/apis/customercard/delete/main.go b/examples/apis/customercard/delete/main.go new file mode 100644 index 00000000..1d627f9e --- /dev/null +++ b/examples/apis/customercard/delete/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "fmt" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/customercard" +) + +func main() { + accessToken := "{{ACCESS_TOKEN}}" + + cfg, err := config.New(accessToken) + if err != nil { + fmt.Println(err) + return + } + + client := customercard.NewClient(cfg) + card, err := client.Delete(context.Background(), "{{CUSTOMER_ID}}", "{{CARD_ID}}") + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(card) +} diff --git a/examples/apis/customercard/get/main.go b/examples/apis/customercard/get/main.go new file mode 100644 index 00000000..e0a314a0 --- /dev/null +++ b/examples/apis/customercard/get/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "fmt" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/customercard" +) + +func main() { + accessToken := "{{ACCESS_TOKEN}}" + + cfg, err := config.New(accessToken) + if err != nil { + fmt.Println(err) + return + } + + client := customercard.NewClient(cfg) + card, err := client.Get(context.Background(), "{{CUSTOMER_ID}}", "{{CARD_ID}}") + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(card) +} diff --git a/examples/apis/customercard/list/main.go b/examples/apis/customercard/list/main.go new file mode 100644 index 00000000..03ff7aa1 --- /dev/null +++ b/examples/apis/customercard/list/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "fmt" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/customercard" +) + +func main() { + accessToken := "{{ACCESS_TOKEN}}" + + cfg, err := config.New(accessToken) + if err != nil { + fmt.Println(err) + return + } + + client := customercard.NewClient(cfg) + cards, err := client.List(context.Background(), "{{CUSTOMER_ID}}") + if err != nil { + fmt.Println(err) + return + } + + for _, c := range cards { + fmt.Println(c) + } +} diff --git a/examples/apis/customercard/update/main.go b/examples/apis/customercard/update/main.go new file mode 100644 index 00000000..882d3f44 --- /dev/null +++ b/examples/apis/customercard/update/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "fmt" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/customercard" +) + +func main() { + accessToken := "{{ACCESS_TOKEN}}" + + cfg, err := config.New(accessToken) + if err != nil { + fmt.Println(err) + return + } + + req := customercard.Request{Token: "{{CARD_TOKEN}}"} + + client := customercard.NewClient(cfg) + card, err := client.Update(context.Background(), "{{CUSTOMER_ID}}", "{{CARD_ID}}", req) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(card) +} diff --git a/pkg/customercard/client.go b/pkg/customercard/client.go new file mode 100644 index 00000000..b6b2bd4d --- /dev/null +++ b/pkg/customercard/client.go @@ -0,0 +1,121 @@ +package customercard + +import ( + "context" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/internal/httpclient" +) + +const ( + urlBase = "https://api.mercadopago.com/v1/customers/{customer_id}/cards" + urlWithID = urlBase + "/{card_id}" +) + +// Client contains the methods to interact with the Customer Cards API. +type Client interface { + // Create a new customer card. + // It is a post request to the endpoint: https://api.mercadopago.com/v1/customer/{customer_id}/cards + // Reference: https://www.mercadopago.com/developers/en/reference/cards/_customers_customer_id_cards/post + Create(ctx context.Context, customerID string, request Request) (*Response, error) + + // Get a customer card by id. + // It is a get request to the endpoint: https://api.mercadopago.com/v1/customer/{customer_id}/cards/{card_id} + // Reference: https://www.mercadopago.com/developers/en/reference/cards/_customers_customer_id_cards_id/get + Get(ctx context.Context, customerID, cardID string) (*Response, error) + + // Update a customer card by id. + // It is a put request to the endpoint: https://api.mercadopago.com/v1/customer/{customer_id}/cards/{card_id} + // Reference: https://www.mercadopago.com/developers/en/reference/cards/_customers_customer_id_cards_id/put + Update(ctx context.Context, customerID, cardID string, request Request) (*Response, error) + + // Delete deletes a customer card by id. + // It is a delete request to the endpoint: https://api.mercadopago.com/v1/customer/{customer_id}/cards/{card_id} + // Reference: https://www.mercadopago.com/developers/en/reference/cards/_customers_customer_id_cards_id/delete + Delete(ctx context.Context, customerID, cardID string) (*Response, error) + + // List all customer cards. + // It is a get request to the endpoint: https://api.mercadopago.com/v1/customer/{customer_id}/cards + // Reference: https://www.mercadopago.com/developers/en/reference/cards/_customers_customer_id_cards/get + List(ctx context.Context, customerID string) ([]Response, error) +} + +// client is the implementation of Client. +type client struct { + config *config.Config +} + +// NewClient returns a new Customer Card Client. +func NewClient(c *config.Config) Client { + return &client{ + config: c, + } +} + +func (c *client) Create(ctx context.Context, customerID string, request Request) (*Response, error) { + params := map[string]string{ + "customer_id": customerID, + } + + res, err := httpclient.Post[Response](ctx, c.config, urlBase, request, httpclient.WithPathParams(params)) + if err != nil { + return nil, err + } + + return res, nil +} + +func (c *client) Get(ctx context.Context, customerID, cardID string) (*Response, error) { + params := map[string]string{ + "customer_id": customerID, + "card_id": cardID, + } + + res, err := httpclient.Get[Response](ctx, c.config, urlWithID, httpclient.WithPathParams(params)) + if err != nil { + return nil, err + } + + return res, nil +} + +func (c *client) Update(ctx context.Context, customerID, cardID string, request Request) (*Response, error) { + params := map[string]string{ + "customer_id": customerID, + "card_id": cardID, + } + + res, err := httpclient.Put[Response](ctx, c.config, urlWithID, request, httpclient.WithPathParams(params)) + if err != nil { + return nil, err + } + + return res, nil +} + +func (c *client) Delete(ctx context.Context, customerID, cardID string) (*Response, error) { + params := map[string]string{ + "customer_id": customerID, + "card_id": cardID, + } + + res, err := httpclient.Delete[Response](ctx, c.config, urlWithID, nil, httpclient.WithPathParams(params)) + if err != nil { + return nil, err + } + + return res, nil +} + +func (c *client) List(ctx context.Context, customerID string) ([]Response, error) { + params := map[string]string{ + "customer_id": customerID, + } + + res, err := httpclient.Get[[]Response](ctx, c.config, urlBase, httpclient.WithPathParams(params)) + if err != nil { + return nil, err + } + + return *res, nil +} diff --git a/pkg/customercard/client_test.go b/pkg/customercard/client_test.go new file mode 100644 index 00000000..f4d7fa96 --- /dev/null +++ b/pkg/customercard/client_test.go @@ -0,0 +1,608 @@ +package customercard + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "reflect" + "strings" + "testing" + "time" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/internal/httpclient" +) + +var ( + responseJSON, _ = os.Open("../../resources/mocks/customer_card/card_response.json") + response, _ = io.ReadAll(responseJSON) + + listResponseJSON, _ = os.Open("../../resources/mocks/customer_card/list_response.json") + listResponse, _ = io.ReadAll(listResponseJSON) +) + +func TestCreate(t *testing.T) { + type fields struct { + config *config.Config + } + type args struct { + ctx context.Context + customerID string + req Request + } + 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(), + customerID: "any", + req: Request{}, + }, + want: nil, + wantErr: "transport level error: some error", + }, + { + name: "should_return_card_response", + fields: fields{ + config: &config.Config{ + Requester: &httpclient.Mock{ + DoMock: func(req *http.Request) (*http.Response, error) { + stringReader := strings.NewReader(string(response)) + stringReadCloser := io.NopCloser(stringReader) + return &http.Response{ + Body: stringReadCloser, + }, nil + }, + }, + }, + }, + args: args{ + ctx: context.Background(), + customerID: "1111111111-pDci63MBohR7c", + req: Request{Token: "938e19c9848b89fb207105b8a17e97ce"}, + }, + want: &Response{ + ID: "9999999999", + CustomerID: "1111111111-pDci63MBohR7c", + UserID: "0000000000", + FirstSixDigits: "123456", + LastFourDigits: "1234", + ExpirationMonth: 12, + ExpirationYear: 2025, + LiveMode: true, + DateCreated: parseDate("2024-02-07T16:28:38.000-04:00"), + DateLastUpdated: parseDate("2024-02-07T16:31:06.964-04:00"), + Issuer: IssuerResponse{ + ID: 24, + Name: "Mastercard", + }, + Cardholder: CardholderResponse{ + Name: "APRO", + Identification: IdentificationResponse{ + Number: "19119119100", + Type: "CPF", + }, + }, + AdditionalInfo: AdditionalInfoResponse{ + RequestPublic: "true", + ApiClientApplication: "traffic-layer", + ApiClientScope: "mapi-pci-tl", + }, + PaymentMethod: PaymentMethodResponse{ + ID: "master", + Name: "Mastercard", + PaymentTypeID: "credit_card", + Thumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + SecureThumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + }, + SecurityCode: SecurityCode{ + Length: 3, + CardLocation: "back", + }, + }, + wantErr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &client{tt.fields.config} + got, err := c.Create(tt.args.ctx, tt.args.customerID, tt.args.req) + gotErr := "" + if err != nil { + gotErr = err.Error() + } + + if gotErr != tt.wantErr { + t.Errorf("client.Create() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("client.Create() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUpdate(t *testing.T) { + type fields struct { + config *config.Config + } + type args struct { + ctx context.Context + customerID string + cardID string + req Request + } + 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(), + customerID: "any", + req: Request{}, + }, + want: nil, + wantErr: "transport level error: some error", + }, + { + name: "should_return_card_response", + fields: fields{ + config: &config.Config{ + Requester: &httpclient.Mock{ + DoMock: func(req *http.Request) (*http.Response, error) { + stringReader := strings.NewReader(string(response)) + stringReadCloser := io.NopCloser(stringReader) + return &http.Response{ + Body: stringReadCloser, + }, nil + }, + }, + }, + }, + args: args{ + ctx: context.Background(), + customerID: "1111111111-pDci63MBohR7c", + cardID: "9999999999", + req: Request{Token: "938e19c9848b89fb207105b8a17e97ce"}, + }, + want: &Response{ + ID: "9999999999", + CustomerID: "1111111111-pDci63MBohR7c", + UserID: "0000000000", + FirstSixDigits: "123456", + LastFourDigits: "1234", + ExpirationMonth: 12, + ExpirationYear: 2025, + LiveMode: true, + DateCreated: parseDate("2024-02-07T16:28:38.000-04:00"), + DateLastUpdated: parseDate("2024-02-07T16:31:06.964-04:00"), + Issuer: IssuerResponse{ + ID: 24, + Name: "Mastercard", + }, + Cardholder: CardholderResponse{ + Name: "APRO", + Identification: IdentificationResponse{ + Number: "19119119100", + Type: "CPF", + }, + }, + AdditionalInfo: AdditionalInfoResponse{ + RequestPublic: "true", + ApiClientApplication: "traffic-layer", + ApiClientScope: "mapi-pci-tl", + }, + PaymentMethod: PaymentMethodResponse{ + ID: "master", + Name: "Mastercard", + PaymentTypeID: "credit_card", + Thumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + SecureThumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + }, + SecurityCode: SecurityCode{ + Length: 3, + CardLocation: "back", + }, + }, + wantErr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &client{tt.fields.config} + got, err := c.Update(tt.args.ctx, tt.args.customerID, tt.args.cardID, tt.args.req) + gotErr := "" + if err != nil { + gotErr = err.Error() + } + + if gotErr != tt.wantErr { + t.Errorf("client.Update() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("client.Update() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGet(t *testing.T) { + type fields struct { + config *config.Config + } + type args struct { + ctx context.Context + customerID string + cardID 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(), + customerID: "any", + }, + want: nil, + wantErr: "transport level error: some error", + }, + { + name: "should_return_card_response", + fields: fields{ + config: &config.Config{ + Requester: &httpclient.Mock{ + DoMock: func(req *http.Request) (*http.Response, error) { + stringReader := strings.NewReader(string(response)) + stringReadCloser := io.NopCloser(stringReader) + return &http.Response{ + Body: stringReadCloser, + }, nil + }, + }, + }, + }, + args: args{ + ctx: context.Background(), + customerID: "1111111111-pDci63MBohR7c", + cardID: "9999999999", + }, + want: &Response{ + ID: "9999999999", + CustomerID: "1111111111-pDci63MBohR7c", + UserID: "0000000000", + FirstSixDigits: "123456", + LastFourDigits: "1234", + ExpirationMonth: 12, + ExpirationYear: 2025, + LiveMode: true, + DateCreated: parseDate("2024-02-07T16:28:38.000-04:00"), + DateLastUpdated: parseDate("2024-02-07T16:31:06.964-04:00"), + Issuer: IssuerResponse{ + ID: 24, + Name: "Mastercard", + }, + Cardholder: CardholderResponse{ + Name: "APRO", + Identification: IdentificationResponse{ + Number: "19119119100", + Type: "CPF", + }, + }, + AdditionalInfo: AdditionalInfoResponse{ + RequestPublic: "true", + ApiClientApplication: "traffic-layer", + ApiClientScope: "mapi-pci-tl", + }, + PaymentMethod: PaymentMethodResponse{ + ID: "master", + Name: "Mastercard", + PaymentTypeID: "credit_card", + Thumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + SecureThumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + }, + SecurityCode: SecurityCode{ + Length: 3, + CardLocation: "back", + }, + }, + wantErr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &client{tt.fields.config} + got, err := c.Get(tt.args.ctx, tt.args.customerID, tt.args.cardID) + 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 TestDelete(t *testing.T) { + type fields struct { + config *config.Config + } + type args struct { + ctx context.Context + customerID string + cardID 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(), + customerID: "any", + }, + want: nil, + wantErr: "transport level error: some error", + }, + { + name: "should_return_card_response", + fields: fields{ + config: &config.Config{ + Requester: &httpclient.Mock{ + DoMock: func(req *http.Request) (*http.Response, error) { + stringReader := strings.NewReader(string(response)) + stringReadCloser := io.NopCloser(stringReader) + return &http.Response{ + Body: stringReadCloser, + }, nil + }, + }, + }, + }, + args: args{ + ctx: context.Background(), + customerID: "1111111111-pDci63MBohR7c", + cardID: "9999999999", + }, + want: &Response{ + ID: "9999999999", + CustomerID: "1111111111-pDci63MBohR7c", + UserID: "0000000000", + FirstSixDigits: "123456", + LastFourDigits: "1234", + ExpirationMonth: 12, + ExpirationYear: 2025, + LiveMode: true, + DateCreated: parseDate("2024-02-07T16:28:38.000-04:00"), + DateLastUpdated: parseDate("2024-02-07T16:31:06.964-04:00"), + Issuer: IssuerResponse{ + ID: 24, + Name: "Mastercard", + }, + Cardholder: CardholderResponse{ + Name: "APRO", + Identification: IdentificationResponse{ + Number: "19119119100", + Type: "CPF", + }, + }, + AdditionalInfo: AdditionalInfoResponse{ + RequestPublic: "true", + ApiClientApplication: "traffic-layer", + ApiClientScope: "mapi-pci-tl", + }, + PaymentMethod: PaymentMethodResponse{ + ID: "master", + Name: "Mastercard", + PaymentTypeID: "credit_card", + Thumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + SecureThumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + }, + SecurityCode: SecurityCode{ + Length: 3, + CardLocation: "back", + }, + }, + wantErr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &client{tt.fields.config} + got, err := c.Delete(tt.args.ctx, tt.args.customerID, tt.args.cardID) + gotErr := "" + if err != nil { + gotErr = err.Error() + } + + if gotErr != tt.wantErr { + t.Errorf("client.Delete() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("client.Delete() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestList(t *testing.T) { + type fields struct { + config *config.Config + } + type args struct { + ctx context.Context + customerID 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(), + customerID: "any", + }, + want: nil, + wantErr: "transport level error: some error", + }, + { + name: "should_return_card_response", + fields: fields{ + config: &config.Config{ + Requester: &httpclient.Mock{ + DoMock: func(req *http.Request) (*http.Response, error) { + stringReader := strings.NewReader(string(listResponse)) + stringReadCloser := io.NopCloser(stringReader) + return &http.Response{ + Body: stringReadCloser, + }, nil + }, + }, + }, + }, + args: args{ + ctx: context.Background(), + customerID: "1111111111-pDci63MBohR7c", + }, + want: []Response{ + { + ID: "9999999999", + CustomerID: "1111111111-pDci63MBohR7c", + UserID: "0000000000", + FirstSixDigits: "123456", + LastFourDigits: "1234", + ExpirationMonth: 12, + ExpirationYear: 2025, + LiveMode: true, + DateCreated: parseDate("2024-02-07T16:28:38.000-04:00"), + DateLastUpdated: parseDate("2024-02-07T16:31:06.964-04:00"), + Issuer: IssuerResponse{ + ID: 24, + Name: "Mastercard", + }, + Cardholder: CardholderResponse{ + Name: "APRO", + Identification: IdentificationResponse{ + Number: "19119119100", + Type: "CPF", + }, + }, + AdditionalInfo: AdditionalInfoResponse{ + RequestPublic: "true", + ApiClientApplication: "traffic-layer", + ApiClientScope: "mapi-pci-tl", + }, + PaymentMethod: PaymentMethodResponse{ + ID: "master", + Name: "Mastercard", + PaymentTypeID: "credit_card", + Thumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + SecureThumbnail: "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + }, + SecurityCode: SecurityCode{ + Length: 3, + CardLocation: "back", + }, + }, + }, + wantErr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &client{tt.fields.config} + got, err := c.List(tt.args.ctx, tt.args.customerID) + gotErr := "" + if err != nil { + gotErr = err.Error() + } + + if gotErr != tt.wantErr { + t.Errorf("client.List() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("client.List() = %v, want %v", got, tt.want) + } + }) + } +} + +func parseDate(s string) *time.Time { + d, _ := time.Parse(time.RFC3339, s) + return &d +} diff --git a/pkg/customercard/request.go b/pkg/customercard/request.go new file mode 100644 index 00000000..66740317 --- /dev/null +++ b/pkg/customercard/request.go @@ -0,0 +1,5 @@ +package customercard + +type Request struct { + Token string `json:"token"` +} diff --git a/pkg/customercard/response.go b/pkg/customercard/response.go new file mode 100644 index 00000000..9af9d329 --- /dev/null +++ b/pkg/customercard/response.go @@ -0,0 +1,65 @@ +package customercard + +import "time" + +// Response represents a customer card. +type Response struct { + ID string `json:"id"` + CustomerID string `json:"customer_id"` + UserID string `json:"user_id"` + CardNumberID string `json:"card_number_id"` + FirstSixDigits string `json:"first_six_digits"` + LastFourDigits string `json:"last_four_digits"` + ExpirationMonth int `json:"expiration_month"` + ExpirationYear int `json:"expiration_year"` + LiveMode bool `json:"live_mode"` + + DateCreated *time.Time `json:"date_created"` + DateLastUpdated *time.Time `json:"date_last_updated"` + Issuer IssuerResponse `json:"issuer"` + Cardholder CardholderResponse `json:"cardholder"` + AdditionalInfo AdditionalInfoResponse `json:"additional_info"` + PaymentMethod PaymentMethodResponse `json:"payment_method"` + SecurityCode SecurityCode `json:"security_code"` +} + +// AdditionalInfoResponse represents additional customer card information. +type AdditionalInfoResponse struct { + RequestPublic string `json:"request_public"` + ApiClientApplication string `json:"api_client_application"` + ApiClientScope string `json:"api_client_scope"` +} + +// CardholderResponse represents information about the cardholder. +type CardholderResponse struct { + Name string `json:"name"` + + Identification IdentificationResponse `json:"identification"` +} + +// IdentificationResponse represents the cardholder's document. +type IdentificationResponse struct { + Number string `json:"number"` + Type string `json:"type"` +} + +// IssuerResponse represents the card issuer code. +type IssuerResponse struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// PaymentMethodResponse represents the card's payment method. +type PaymentMethodResponse struct { + ID string `json:"id"` + Name string `json:"name"` + PaymentTypeID string `json:"payment_type_id"` + Thumbnail string `json:"thumbnail"` + SecureThumbnail string `json:"secure_thumbnail"` +} + +// SecurityCode represents the card's security code. +type SecurityCode struct { + Length int `json:"length"` + CardLocation string `json:"card_location"` +} diff --git a/pkg/internal/httpclient/client.go b/pkg/internal/httpclient/client.go index 18c8c864..a2008495 100644 --- a/pkg/internal/httpclient/client.go +++ b/pkg/internal/httpclient/client.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "runtime" "strings" @@ -40,8 +41,8 @@ var ( // Get makes requests with the GET method // Will return the struct specified in Generics -func Get[T any](ctx context.Context, cfg *config.Config, path string) (*T, error) { - req, err := makeRequest(ctx, cfg, http.MethodGet, path, nil) +func Get[T any](ctx context.Context, cfg *config.Config, url string, opts ...Option) (*T, error) { + req, err := makeRequest(ctx, cfg, http.MethodGet, url, nil, opts...) if err != nil { return nil, err } @@ -51,8 +52,8 @@ func Get[T any](ctx context.Context, cfg *config.Config, path string) (*T, error // Post makes requests with the POST method // Will return the struct specified in Generics -func Post[T any](ctx context.Context, cfg *config.Config, path string, body any) (*T, error) { - req, err := makeRequest(ctx, cfg, http.MethodPost, path, body) +func Post[T any](ctx context.Context, cfg *config.Config, url string, body any, opts ...Option) (*T, error) { + req, err := makeRequest(ctx, cfg, http.MethodPost, url, body, opts...) if err != nil { return nil, err } @@ -62,8 +63,8 @@ func Post[T any](ctx context.Context, cfg *config.Config, path string, body any) // Put makes requests with the PUT method // Will return the struct specified in Generics -func Put[T any](ctx context.Context, cfg *config.Config, path string, body any) (*T, error) { - req, err := makeRequest(ctx, cfg, http.MethodPut, path, body) +func Put[T any](ctx context.Context, cfg *config.Config, url string, body any, opts ...Option) (*T, error) { + req, err := makeRequest(ctx, cfg, http.MethodPut, url, body, opts...) if err != nil { return nil, err } @@ -73,8 +74,8 @@ func Put[T any](ctx context.Context, cfg *config.Config, path string, body any) // Delete makes requests with the DELETE method // Will return the struct specified in Generics -func Delete[T any](ctx context.Context, cfg *config.Config, path string, body any) (*T, error) { - req, err := makeRequest(ctx, cfg, http.MethodDelete, path, body) +func Delete[T any](ctx context.Context, cfg *config.Config, url string, body any, opts ...Option) (*T, error) { + req, err := makeRequest(ctx, cfg, http.MethodDelete, url, body, opts...) if err != nil { return nil, err } @@ -82,13 +83,24 @@ func Delete[T any](ctx context.Context, cfg *config.Config, path string, body an return send[T](cfg.Requester, req) } -func makeRequest(ctx context.Context, cfg *config.Config, method, path string, body any) (*http.Request, error) { - req, err := buildHTTPRequest(ctx, method, path, body) +func makeRequest(ctx context.Context, cfg *config.Config, method, url string, body any, opts ...Option) (*http.Request, error) { + req, err := buildHTTPRequest(ctx, method, url, body) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } + // Apply all the functional options to configure the client. + opt := clientOption{} + for _, o := range opts { + o(opt) + } + makeHeaders(req, cfg) + makeQueryParams(req, opt.queryParams) + + if err = makePathParams(req, opt.pathParams); err != nil { + return nil, err + } return req, nil } @@ -135,3 +147,45 @@ func buildBody(body any) (io.Reader, error) { return strings.NewReader(string(b)), nil } + +func makePathParams(req *http.Request, params map[string]string) error { + pathURL := req.URL.Path + + for k, v := range params { + pathParam := ":" + k + pathURL = strings.Replace(pathURL, pathParam, v, 1) + } + + if err := validatePathParams(pathURL); err != nil { + return err + } + + req.URL.Path = pathURL + + return nil +} + +func makeQueryParams(req *http.Request, params map[string]string) { + queryParams := url.Values{} + + for k, v := range params { + queryParams.Add(k, v) + } + + req.URL.RawQuery = queryParams.Encode() +} + +func validatePathParams(pathURL string) error { + if strings.Contains(pathURL, ":") { + words := strings.Split(pathURL, "/") + var paramsNotReplaced []string + for _, word := range words { + if strings.Contains(word, ":") { + paramsNotReplaced = append(paramsNotReplaced, strings.Replace(word, ":", "", 1)) + } + } + return fmt.Errorf("path parameters not informed: %s", strings.Join(paramsNotReplaced, ",")) + } + + return nil +} diff --git a/pkg/internal/httpclient/client_option.go b/pkg/internal/httpclient/client_option.go new file mode 100644 index 00000000..fafd98e1 --- /dev/null +++ b/pkg/internal/httpclient/client_option.go @@ -0,0 +1,27 @@ +package httpclient + +// ClientOption allows sending options in the http client +type clientOption struct { + pathParams map[string]string + queryParams map[string]string +} + +type Option func(clientOption) + +// WithPathParams allows sending path parameters in the request. +func WithPathParams(params map[string]string) Option { + return func(c clientOption) { + if params != nil { + c.pathParams = params + } + } +} + +// WithQueryParams allows sending query parameters in the request. +func WithQueryParams(params map[string]string) Option { + return func(c clientOption) { + if params != nil { + c.queryParams = params + } + } +} diff --git a/pkg/internal/httpclient/client_test.go b/pkg/internal/httpclient/client_test.go new file mode 100644 index 00000000..9e06bd0d --- /dev/null +++ b/pkg/internal/httpclient/client_test.go @@ -0,0 +1,127 @@ +package httpclient + +import ( + "net/http" + "testing" +) + +func TestMakePathParams(t *testing.T) { + type args struct { + url string + params map[string]string + } + tests := []struct { + name string + args args + wantURL string + wantMsgErr string + }{ + { + name: "should_replace_one_path_param", + args: args{ + url: "http://localhost/payments/:payment_id", + params: map[string]string{ + "payment_id": "1234567890", + }, + }, + wantURL: "http://localhost/payments/1234567890", + }, + { + name: "should_return_path_param_not_informed", + args: args{ + url: "http://localhost/customers/:customer_id", + params: map[string]string{ + "payment_id": "1234567890", + }, + }, + wantMsgErr: "path parameters not informed: customer_id", + }, + { + name: "should_return_two_path_params_not_informed", + args: args{ + url: "http://localhost/tests/:test_id/units/:unit_id", + params: map[string]string{ + "integrate_id": "1234567890", + }, + }, + wantMsgErr: "path parameters not informed: test_id,unit_id", + }, + { + name: "should_return_the_same_path_url", + args: args{ + url: "http://localhost/tests/", + params: map[string]string{ + "integrate_id": "1234567890", + }, + }, + wantURL: "http://localhost/tests/", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, tt.args.url, nil) + + err := makePathParams(req, tt.args.params) + if err != nil && err.Error() != tt.wantMsgErr { + t.Errorf("makeParams() msgError = %v, wantMsgErr %v", err.Error(), tt.wantMsgErr) + return + } + + if err == nil && tt.wantURL != req.URL.String() { + t.Errorf("makeParams() wantURL = %v, gotURL %v", tt.wantURL, req.URL.String()) + } + }) + } +} + +func TestMakeQueryParams(t *testing.T) { + type args struct { + url string + params map[string]string + } + tests := []struct { + name string + args args + wantURL string + }{ + { + name: "should_add_one_query_param", + args: args{ + url: "http://localhost/payments/1234567890/search", + params: map[string]string{ + "external_resource": "as2f12345", + }, + }, + wantURL: "http://localhost/payments/1234567890/search?external_resource=as2f12345", + }, + { + name: "should_add_two_query_params", + args: args{ + url: "http://localhost/payments/1234567890/search", + params: map[string]string{ + "external_resource": "as2f12345", + "offset": "2", + }, + }, + wantURL: "http://localhost/payments/1234567890/search?external_resource=as2f12345&offset=2", + }, + { + name: "should_return_the_same_path_url", + args: args{ + url: "http://localhost/tests/", + params: map[string]string{}, + }, + wantURL: "http://localhost/tests/", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, tt.args.url, nil) + + makeQueryParams(req, tt.args.params) + if tt.wantURL != req.URL.String() { + t.Errorf("makeQueryParams() wantURL = %v, gotURL %v", tt.wantURL, req.URL.String()) + } + }) + } +} diff --git a/resources/mocks/customer_card/card_response.json b/resources/mocks/customer_card/card_response.json new file mode 100644 index 00000000..40675ba4 --- /dev/null +++ b/resources/mocks/customer_card/card_response.json @@ -0,0 +1,40 @@ +{ + "additional_info": { + "request_public": "true", + "api_client_application": "traffic-layer", + "api_client_scope": "mapi-pci-tl" + }, + "card_number_id": null, + "cardholder": { + "name": "APRO", + "identification": { + "number": "19119119100", + "type": "CPF" + } + }, + "customer_id": "1111111111-pDci63MBohR7c", + "date_created": "2024-02-07T16:28:38.000-04:00", + "date_last_updated": "2024-02-07T16:31:06.964-04:00", + "expiration_month": 12, + "expiration_year": 2025, + "first_six_digits": "123456", + "id": "9999999999", + "issuer": { + "id": 24, + "name": "Mastercard" + }, + "last_four_digits": "1234", + "live_mode": true, + "payment_method": { + "id": "master", + "name": "Mastercard", + "payment_type_id": "credit_card", + "thumbnail": "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + "secure_thumbnail": "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png" + }, + "security_code": { + "length": 3, + "card_location": "back" + }, + "user_id": "0000000000" +} \ No newline at end of file diff --git a/resources/mocks/customer_card/list_response.json b/resources/mocks/customer_card/list_response.json new file mode 100644 index 00000000..92202c39 --- /dev/null +++ b/resources/mocks/customer_card/list_response.json @@ -0,0 +1,42 @@ +[ + { + "additional_info": { + "request_public": "true", + "api_client_application": "traffic-layer", + "api_client_scope": "mapi-pci-tl" + }, + "card_number_id": null, + "cardholder": { + "name": "APRO", + "identification": { + "number": "19119119100", + "type": "CPF" + } + }, + "customer_id": "1111111111-pDci63MBohR7c", + "date_created": "2024-02-07T16:28:38.000-04:00", + "date_last_updated": "2024-02-07T16:31:06.964-04:00", + "expiration_month": 12, + "expiration_year": 2025, + "first_six_digits": "123456", + "id": "9999999999", + "issuer": { + "id": 24, + "name": "Mastercard" + }, + "last_four_digits": "1234", + "live_mode": true, + "payment_method": { + "id": "master", + "name": "Mastercard", + "payment_type_id": "credit_card", + "thumbnail": "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png", + "secure_thumbnail": "https://http2.mlstatic.com/storage/logos-api-admin/0daa1670-5c81-11ec-ae75-df2bef173be2-xl@2x.png" + }, + "security_code": { + "length": 3, + "card_location": "back" + }, + "user_id": "0000000000" + } +] \ No newline at end of file diff --git a/test/integration/payment_method/payment_method_test.go b/test/integration/payment_method/payment_method_test.go index 92aee1a9..b72da871 100644 --- a/test/integration/payment_method/payment_method_test.go +++ b/test/integration/payment_method/payment_method_test.go @@ -11,7 +11,7 @@ import ( func TestPaymentMethod(t *testing.T) { t.Run("should_list_payment_methods", func(t *testing.T) { - cfg, err := config.New(os.Getenv("at")) + cfg, err := config.New(os.Getenv("ACCESS_TOKEN")) if err != nil { t.Fatal(err) }