diff --git a/account_payment_methods.go b/account_payment_methods.go new file mode 100644 index 000000000..2afcc3bc6 --- /dev/null +++ b/account_payment_methods.go @@ -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 +} diff --git a/account_payments.go b/account_payments.go index 452f53f16..dc43c008e 100644 --- a/account_payments.go +++ b/account_payments.go @@ -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 diff --git a/account_promo_credits.go b/account_promo_credits.go new file mode 100644 index 000000000..a40dc243d --- /dev/null +++ b/account_promo_credits.go @@ -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) +} diff --git a/test/unit/account_payment_methods_test.go b/test/unit/account_payment_methods_test.go new file mode 100644 index 000000000..277d1ffc1 --- /dev/null +++ b/test/unit/account_payment_methods_test.go @@ -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) + } +} diff --git a/test/unit/account_payments_test.go b/test/unit/account_payments_test.go new file mode 100644 index 000000000..c78d6f9e5 --- /dev/null +++ b/test/unit/account_payments_test.go @@ -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) +} diff --git a/test/unit/account_promo_credits_test.go b/test/unit/account_promo_credits_test.go new file mode 100644 index 000000000..eaf1513fb --- /dev/null +++ b/test/unit/account_promo_credits_test.go @@ -0,0 +1,36 @@ +package unit + +import ( + "context" + "github.com/linode/linodego" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAccountPromoCredits_Add(t *testing.T) { + fixtureData, err := fixtures.GetFixture("account_promo_credits_add_promo_code") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockPost("account/promo-codes", fixtureData) + + requestData := linodego.PromoCodeCreateOptions{ + PromoCode: "supercoolpromocode", + } + + promoCode, err := base.Client.AddPromoCode(context.Background(), requestData) + if err != nil { + t.Fatalf("Error adding promo code: %v", err) + } + + assert.Equal(t, "10.00", promoCode.CreditMonthlyCap) + assert.Equal(t, "50.00", promoCode.CreditRemaining) + assert.Equal(t, "Receive up to $10 off your services every month for 6 months! Unused credits will expire once this promotion period ends.", promoCode.Description) + assert.Equal(t, "https://linode.com/10_a_month_promotion.svg", promoCode.ImageURL) + assert.Equal(t, "all", promoCode.ServiceType) + assert.Equal(t, "$10 off your Linode a month!", promoCode.Summary) + assert.Equal(t, "10.00", promoCode.ThisMonthCreditRemaining) +} diff --git a/test/unit/fixtures/account_payment_create.json b/test/unit/fixtures/account_payment_create.json new file mode 100644 index 000000000..585c8c276 --- /dev/null +++ b/test/unit/fixtures/account_payment_create.json @@ -0,0 +1,5 @@ +{ + "date": "2018-01-15T00:01:01", + "id": 123, + "usd": "120.50" +} \ No newline at end of file diff --git a/test/unit/fixtures/account_payment_methods_get.json b/test/unit/fixtures/account_payment_methods_get.json new file mode 100644 index 000000000..0c9ad45e1 --- /dev/null +++ b/test/unit/fixtures/account_payment_methods_get.json @@ -0,0 +1,11 @@ +{ + "created": "2018-01-15T00:01:01", + "data": { + "card_type": "Discover", + "expiry": "06/2022", + "last_four": "1234" + }, + "id": 123, + "is_default": true, + "type": "credit_card" +} \ No newline at end of file diff --git a/test/unit/fixtures/account_payment_methods_list.json b/test/unit/fixtures/account_payment_methods_list.json new file mode 100644 index 000000000..9e0be04b2 --- /dev/null +++ b/test/unit/fixtures/account_payment_methods_list.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "created": "2018-01-15T00:01:01", + "data": { + "card_type": "Discover", + "expiry": "06/2022", + "last_four": "1234" + }, + "id": 123, + "is_default": true, + "type": "credit_card" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} \ No newline at end of file diff --git a/test/unit/fixtures/account_promo_credits_add_promo_code.json b/test/unit/fixtures/account_promo_credits_add_promo_code.json new file mode 100644 index 000000000..1bd43696e --- /dev/null +++ b/test/unit/fixtures/account_promo_credits_add_promo_code.json @@ -0,0 +1,10 @@ +{ + "credit_monthly_cap": "10.00", + "credit_remaining": "50.00", + "description": "Receive up to $10 off your services every month for 6 months! Unused credits will expire once this promotion period ends.", + "expire_dt": "2018-01-31T23:59:59", + "image_url": "https://linode.com/10_a_month_promotion.svg", + "service_type": "all", + "summary": "$10 off your Linode a month!", + "this_month_credit_remaining": "10.00" +} \ No newline at end of file