Skip to content

Commit

Permalink
Added support for missing Payment-related endpoints (linode#601)
Browse files Browse the repository at this point in the history
* Added support for missing payment endpoints

* Rename PromoCode struct to Promotion
  • Loading branch information
ezilber-akamai authored Nov 6, 2024
1 parent c6b4678 commit f96575c
Show file tree
Hide file tree
Showing 10 changed files with 448 additions and 1 deletion.
165 changes: 165 additions & 0 deletions account_payment_methods.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package linodego

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/linode/linodego/internal/parseabletime"
)

// PaymentMethod represents a PaymentMethod object
type PaymentMethod struct {
// The unique ID of the Payment Method.
ID int `json:"id"`

// When the Payment Method was created.
Created *time.Time `json:"created"`

// Whether this Payment Method is the default method for automatically processing service charges.
IsDefault bool `json:"is_default"`

// The type of Payment Method.
Type string `json:"type"`

// The detailed data for the Payment Method, which can be of varying types.
Data interface{} `json:"data"`
}

// PaymentMethodDataCreditCard represents a PaymentMethodDataCreditCard object
type PaymentMethodDataCreditCard struct {
// The type of credit card.
CardType string `json:"card_type"`

// The expiration month and year of the credit card.
Expiry string `json:"expiry"`

// The last four digits of the credit card number.
LastFour string `json:"last_four"`
}

// PaymentMethodDataGooglePay represents a PaymentMethodDataGooglePay object
type PaymentMethodDataGooglePay struct {
// The type of credit card.
CardType string `json:"card_type"`

// The expiration month and year of the credit card.
Expiry string `json:"expiry"`

// The last four digits of the credit card number.
LastFour string `json:"last_four"`
}

// PaymentMethodDataPaypal represents a PaymentMethodDataPaypal object
type PaymentMethodDataPaypal struct {
// The email address associated with your PayPal account.
Email string `json:"email"`

// PayPal Merchant ID associated with your PayPal account.
PaypalID string `json:"paypal_id"`
}

// PaymentMethodCreateOptions fields are those accepted by CreatePaymentMethod
type PaymentMethodCreateOptions struct {
// Whether this Payment Method is the default method for automatically processing service charges.
IsDefault bool `json:"is_default"`

// The type of Payment Method. Alternative payment methods including Google Pay and PayPal can be added using the Cloud Manager.
Type string `json:"type"`

// An object representing the credit card information you have on file with Linode to make Payments against your Account.
Data *PaymentMethodCreateOptionsData `json:"data"`
}

type PaymentMethodCreateOptionsData struct {
// Your credit card number. No spaces or hyphens (-) allowed.
CardNumber string `json:"card_number"`

// CVV (Card Verification Value) of the credit card, typically found on the back of the card.
CVV string `json:"cvv"`

// A value from 1-12 representing the expiration month of your credit card.
ExpiryMonth int `json:"expiry_month"`

// A four-digit integer representing the expiration year of your credit card.
ExpiryYear int `json:"expiry_year"`
}

// UnmarshalJSON implements the json.Unmarshaler interface
func (i *PaymentMethod) UnmarshalJSON(b []byte) error {
if len(b) == 0 || string(b) == "{}" || string(b) == "null" {
return nil
}

type Mask PaymentMethod

pm := &struct {
*Mask
Created *parseabletime.ParseableTime `json:"created"`
Data json.RawMessage `json:"data"`
}{
Mask: (*Mask)(i),
}

if err := json.Unmarshal(b, &pm); err != nil {
return err
}

// Process Data based on the Type field
switch i.Type {
case "credit_card":
var creditCardData PaymentMethodDataCreditCard
if err := json.Unmarshal(pm.Data, &creditCardData); err != nil {
return err
}
i.Data = creditCardData
case "google_pay":
var googlePayData PaymentMethodDataGooglePay
if err := json.Unmarshal(pm.Data, &googlePayData); err != nil {
return err
}
i.Data = googlePayData
case "paypal":
var paypalData PaymentMethodDataPaypal
if err := json.Unmarshal(pm.Data, &paypalData); err != nil {
return err
}
i.Data = paypalData
default:
return fmt.Errorf("unknown payment method type: %s", i.Type)
}

i.Created = (*time.Time)(pm.Created)
return nil
}

// ListPaymentMethods lists PaymentMethods
func (c *Client) ListPaymentMethods(ctx context.Context, opts *ListOptions) ([]PaymentMethod, error) {
return getPaginatedResults[PaymentMethod](ctx, c, "account/payment-methods", opts)
}

// GetPaymentMethod gets the payment method with the provided ID
func (c *Client) GetPaymentMethod(ctx context.Context, paymentMethodID int) (*PaymentMethod, error) {
e := formatAPIPath("account/payment-methods/%d", paymentMethodID)
return doGETRequest[PaymentMethod](ctx, c, e)
}

// DeletePaymentMethod deletes the payment method with the provided ID
func (c *Client) DeletePaymentMethod(ctx context.Context, paymentMethodID int) error {
e := formatAPIPath("account/payment-methods/%d", paymentMethodID)
return doDELETERequest(ctx, c, e)
}

// AddPaymentMethod adds the provided payment method to the account
func (c *Client) AddPaymentMethod(ctx context.Context, opts PaymentMethodCreateOptions) error {
_, err := doPOSTRequest[PaymentMethod, any](ctx, c, "account/payment-methods", opts)
return err
}

// SetDefaultPaymentMethod sets the payment method with the provided ID as the default
func (c *Client) SetDefaultPaymentMethod(ctx context.Context, paymentMethodID int) error {
e := formatAPIPath("account/payment-methods/%d", paymentMethodID)
_, err := doPOSTRequest[PaymentMethod, any](ctx, c, e)
return err
}
2 changes: 1 addition & 1 deletion account_payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (c *Client) GetPayment(ctx context.Context, paymentID int) (*Payment, error

// CreatePayment creates a Payment
func (c *Client) CreatePayment(ctx context.Context, opts PaymentCreateOptions) (*Payment, error) {
e := "accounts/payments"
e := "account/payments"
response, err := doPOSTRequest[Payment](ctx, c, e, opts)
if err != nil {
return nil, err
Expand Down
67 changes: 67 additions & 0 deletions account_promo_credits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package linodego

import (
"context"
"encoding/json"
"time"

"github.com/linode/linodego/internal/parseabletime"
)

// Promotion represents a Promotion object
type Promotion struct {
// The amount available to spend per month.
CreditMonthlyCap string `json:"credit_monthly_cap"`

// The total amount of credit left for this promotion.
CreditRemaining string `json:"credit_remaining"`

// A detailed description of this promotion.
Description string `json:"description"`

// When this promotion's credits expire.
ExpirationDate *time.Time `json:"-"`

// The location of an image for this promotion.
ImageURL string `json:"image_url"`

// The service to which this promotion applies.
ServiceType string `json:"service_type"`

// Short details of this promotion.
Summary string `json:"summary"`

// The amount of credit left for this month for this promotion.
ThisMonthCreditRemaining string `json:"this_month_credit_remaining"`
}

// PromoCodeCreateOptions fields are those accepted by AddPromoCode
type PromoCodeCreateOptions struct {
// The Promo Code.
PromoCode string `json:"promo_code"`
}

// UnmarshalJSON implements the json.Unmarshaler interface
func (i *Promotion) UnmarshalJSON(b []byte) error {
type Mask Promotion

p := struct {
*Mask
ExpirationDate *parseabletime.ParseableTime `json:"date"`
}{
Mask: (*Mask)(i),
}

if err := json.Unmarshal(b, &p); err != nil {
return err
}

i.ExpirationDate = (*time.Time)(p.ExpirationDate)

return nil
}

// AddPromoCode adds the provided promo code to the account
func (c *Client) AddPromoCode(ctx context.Context, opts PromoCodeCreateOptions) (*Promotion, error) {
return doPOSTRequest[Promotion, any](ctx, c, "account/promo-codes", opts)
}
102 changes: 102 additions & 0 deletions test/unit/account_payment_methods_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package unit

import (
"context"
"github.com/jarcoal/httpmock"
"github.com/linode/linodego"
"github.com/stretchr/testify/assert"
"testing"
)

func TestAccountPaymentMethods_Get(t *testing.T) {
fixtureData, err := fixtures.GetFixture("account_payment_methods_get")
assert.NoError(t, err)

var base ClientBaseCase
base.SetUp(t)
defer base.TearDown(t)

base.MockGet("account/payment-methods/123", fixtureData)

pm, err := base.Client.GetPaymentMethod(context.Background(), 123)
assert.NoError(t, err)

assert.Equal(t, 123, pm.ID)
assert.Equal(t, true, pm.IsDefault)
assert.Equal(t, "credit_card", pm.Type)
assert.Equal(t, linodego.PaymentMethodDataCreditCard{
CardType: "Discover",
Expiry: "06/2022",
LastFour: "1234",
}, pm.Data)
}

func TestAccountPaymentMethods_List(t *testing.T) {
fixtureData, err := fixtures.GetFixture("account_payment_methods_list")
assert.NoError(t, err)

var base ClientBaseCase
base.SetUp(t)
defer base.TearDown(t)

base.MockGet("account/payment-methods", fixtureData)

methods, err := base.Client.ListPaymentMethods(context.Background(), nil)
assert.NoError(t, err)

assert.Equal(t, 1, len(methods))
pm := methods[0]
assert.Equal(t, 123, pm.ID)
assert.Equal(t, true, pm.IsDefault)
assert.Equal(t, "credit_card", pm.Type)
assert.Equal(t, linodego.PaymentMethodDataCreditCard{
CardType: "Discover",
Expiry: "06/2022",
LastFour: "1234",
}, pm.Data)
}

func TestAccountPaymentMethods_Add(t *testing.T) {
client := createMockClient(t)

card := linodego.PaymentMethodCreateOptionsData{
CardNumber: "1234123412341234",
CVV: "123",
ExpiryMonth: 3,
ExpiryYear: 2028,
}

requestData := linodego.PaymentMethodCreateOptions{
Data: &card,
IsDefault: true,
Type: "credit_card",
}

httpmock.RegisterRegexpResponder("POST", mockRequestURL(t, "account/payment-methods"),
mockRequestBodyValidate(t, requestData, nil))

if err := client.AddPaymentMethod(context.Background(), requestData); err != nil {
t.Fatal(err)
}
}

func TestAccountPaymentMethods_Delete(t *testing.T) {
client := createMockClient(t)

httpmock.RegisterRegexpResponder("DELETE", mockRequestURL(t, "account/payment-methods/123"), httpmock.NewStringResponder(200, "{}"))

if err := client.DeletePaymentMethod(context.Background(), 123); err != nil {
t.Fatal(err)
}
}

func TestAccountPaymentMethods_SetDefault(t *testing.T) {
client := createMockClient(t)

httpmock.RegisterRegexpResponder("POST", mockRequestURL(t, "account/payment-methods/123"),
httpmock.NewStringResponder(200, "{}"))

if err := client.SetDefaultPaymentMethod(context.Background(), 123); err != nil {
t.Fatal(err)
}
}
33 changes: 33 additions & 0 deletions test/unit/account_payments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package unit

import (
"context"
"encoding/json"
"github.com/linode/linodego"
"github.com/stretchr/testify/assert"
"testing"
)

func TestAccountPayments_Create(t *testing.T) {
fixtureData, err := fixtures.GetFixture("account_payment_create")
assert.NoError(t, err)

var base ClientBaseCase
base.SetUp(t)
defer base.TearDown(t)

base.MockPost("account/payments", fixtureData)

requestData := linodego.PaymentCreateOptions{
CVV: "123",
USD: json.Number("120.50"),
}

payment, err := base.Client.CreatePayment(context.Background(), requestData)
if err != nil {
t.Fatalf("Error creating payment: %v", err)
}

assert.Equal(t, 123, payment.ID)
assert.Equal(t, json.Number("120.50"), payment.USD)
}
Loading

0 comments on commit f96575c

Please sign in to comment.