Skip to content
cristiangraz edited this page May 29, 2019 · 9 revisions

Migrating to v1

If you have been using this library and recently run into a set of breaking changes, this guide will help you migrate your code to v1 where things will be stable. We highly recommend upgrading your code to this version as several bugs have been fixed and your own code will be much simpler and more maintainable after migrating.

The reason for the upgrade? Design and maintenance issues causing too much complexity for users of the library, particularly around error handling and return values. v1 addresses this and simplifies usage of the library.

Additionally, the internals of the library have been completely rewritten with several bugs fixed and more stable code. The library is now easier to use, easier to maintain, and simpler for the community to add contributions.

This guide will walk you through migrating to the new v1 version of this library step-by-step.

NOTE: If you do not want to (or are not ready to) upgrade, you can pin this library to v0.1 which was released from the latest master before v1 was released.

NOTE: This guide does not cover any changes to internals as they are not applicable for end users. Most of the changes should be straightforward, however, if you are a contributor and have any questions/comments, please open an issue.

Major changes:

  • Remove third argument from recurly.NewClient()
  • Support the context package
  • Simplify error handling
  • Eliminate helper methods that increased the surface area of the library
  • Improve method names
  • Remove deprecated functions and fields
  • Standardize and simplify pagination
  • Add additional fields and functions that were previously not implemented
  • Rework NullInt, NullBool, and NullTime to reduce common implementation bugs
  • Return nil, nil for all Get() methods when the item is not found

This migration guide will walk you through updating your code to address all of these changes. We will first start out with high-level, library-wide changes and then explore changes by service.

Drop third argument from recurly.NewClient()

The third parameter previously was an optional HTTP Client. If you need to set your own HTTP client, you can set directly:

client := recurly.NewClient("your-subdomain", "APIKEY")
client.Client = &http.Client{}

Support the context package

Virtually every method now takes context.Context as the first parameter. You'll either need to pass your own context into the function, or use context.Background() if you do not have one.

Simplify error handling

Previously each function returned *recurly.Response, along with the concrete type and error. For example, creating an account previously looked like this:

resp, account, err := client.Accounts.Create(recurly.Account{})
if err != nil {
   return err
} else if resp.IsClientError() {
   // bad request
} else if resp.IsServerError() {
   // server error
} else {
   // account was successfully created
}

This is unnecessarily complex and error prone: there are too many return values and just determining if the request succeeded is overly complex.

So what changed?

First, the resp parameter is dropped. Next, you can determine if the request succeeded simply by checking the error message. So you can change your code to this:

account, err := client.Accounts.Create(recurly.Account{})
if err != nil {
   return err
}
// account was successfully created

In the majority of cases, this is all you need to do!

If you had any code that generated transactions, you might previously look for a transaction error like this:

resp, purchase, err := client.Purchases.Create(recurly.Purchase{})
if err != nil {
   return err
} else if resp != nil && resp.IsClientError() && resp.StatusCode == http.StatusUnprocessableEntity {
   // Attempt to access the transaction error on resp
   if resp.Transaction != nil && resp.Transaction.TransactionError != nil {
      // handle transaction error
   }
} else if err := wrapError(resp); err != nil { // look for client/server errors in resp
   return err
}

// succeeded

That can now be simplified to:

purchase, err := client.Purchases.Create(context.Background(), recurly.Purchase{})
if e, ok := err.(*recurly.TransactionFailedError); ok {
   // Transaction failed
   // e.Transaction may be set
   // e.TransactionError holds the details of why the transaction failed
} else if err != nil {
   // Catch all for other errors
   return err
}

This is the standard way to check for transaction errors -- regardless of the specific API call!

If you need to inspect validation errors, you can look for *recurly.ClientError. See the README or godoc for details.

Standardize and simplify pagination

Pagination is much simpler now, and it is consistent for all methods that are named List() or List*(). Here is an example of how to paginate accounts:

// Initialize a pager with any pagination options needed.
pager := client.Accounts.List(&recurly.PagerOptions{
	State: recurly.AccountStateActive,
})

// Count the records (if desired)
count, err := pager.Count(ctx)
if err != nil {
	return err
}

// Or iterate through each of the pages
for pager.Next() {
	var accounts []recurly.Account
	if err := pager.Fetch(ctx, &accounts); err != nil {
		return err
	}
}

You can also let the library paginate for you and return all of the results at once:

pager := client.Accounts.List(nil)
var accounts []recurly.Account
if err := pager.FetchAll(ctx, &accounts); err != nil {
	return err
}

Null types

Many users (including myself) have done this thinking they would get a valid value of 0 to be sent to recurly: recurly.NullInt{Int: 0} without realizing that it should have been recurly.NullInt{Int: 0, Valid: true}.

In order to simplify, the actual value and valid bool on all Null types has been made unexported. You can create any of the types now like so:

recurly.NewInt(0)
recurly.NewBool(true)
recurly.NewTime(time.Now())

// Or if you have a pointer: validity is determined on if the pointer is non-nil
recurly.NewIntPtr(v)
recurly.NewBoolPtr(v)
recurly.NewTimePtr(v) // v must be non-nil and v.IsZero() must return false to be considered valid

In addition, each has an Int() Bool() or Time() function (respectively) to access the underlying value:

recurly.NewInt(0).Int() // 0
recurly.NewBool(true).Bool() // true
recurly.NewTime(time.Now()).Time() // time.Time, previously *time.Time

Or to retrieve pointer values (where nil is returned if the value is invalid)

recurly.NewInt(0).IntPtr() // int ptr with a value of 0
recurly.NewBool(true).BoolPtr() // bool ptr with a value of true
recurly.NewTime(time.Now()).TimePtr() // time ptr with a value of time.Now()

If you need to also know if the valid held is valid, you can use the Value() function:

value, ok := recurly.NewInt(0).Value() // 0, true
value, ok = recurly.NewBool(false).Value() // false, true
value, ok = recurly.NewTime(time.Now()).Value() // time.Time, true

Here is what the return values look like for invalid null values (we'll use a zero value to illustrate)

var nullInt recurly.NullInt
nullInt.Int() // returns 0
nullInt.Value() // returns 0, false
nullInt.IntPtr() // returns nil

Return nil, nil for all Get() methods when the item is not found

When retrieving an individual item (such as an account, invoice, or subscription): if the item is not found, a nil item and nil error will be returned. This is standard for all functions named Get(). All other functions would return a ClientError when encountering a 404 Not Found status code.

a, err := client.Accounts.Get(ctx, "1")
if err != nil {
    return err
} else if a == nil {
    // Account not found
}

Service-level changes

Now we'll look at the specific changes that occurred for each service. The godoc for each of the services will give you more details than this section, but this will call out any important changes.

API Version

Updated from v2.18 to v2.20

Accounts

  • client.Accounts.LookupAccountBalance() renamed to client.Accounts.Balance()
  • The following changes have been made to the Account struct:
    • Address changed from recurly.Address to *recurly.Address
    • Added CCEmails
    • Added HasLiveSubscription
    • Added HasActiveSubscription
    • Added HasFutureSubscription
    • Added HasCanceledsubscription
    • Added HasPastDueInvoice
  • AccountCode field was removed from the AccountBalance struct

AddOns

No significant changes.

Adjustments

  • client.Adjustments.List() renamed to client.Adjustments.ListAccount()
  • The following changes have been made to the Adjustments struct
    • UnitAmountInCents changed from int to NullInt

Billing

  • ExternalHPPType added to Billing struct
  • client.Billing.CreateWithToken() removed. Please use client.Billing.Create(context.Background(), recurly.Billing{Token: "TOKEN"}
  • client.Billing.UpdateWithToken() removed. Please use client.Billing.Update(context.Background(), recurly.Billing{Token: "TOKEN"}

Coupons

  • Added new methods:
    • client.Coupons.Update()
    • client.Coupons.Restore()
    • client.Coupons.Generate()
  • The following changes have been made to the Coupon struct:
    • ID changed from uint64 to int64
    • SingleUse field was removed from the Coupon struct (deprecated by Recurly)

Credit Payments

No significant changes.

Invoices

  • client.Invoices.RefundVoidOpenAmount changed to accept an InvoiceRefund struct with parameters needed to refund. Move amountInCents and refundMethod moved to InvoiceRefund struct. InvoiceRefund struct allows you to send additional parameters previously not allowed.
  • Added client.Invoices.RefundVoidLineItems to refund specific line items

Plans

No significant changes.

Purchases

  • Added the following methods:
    • client.Purchases.Authorize()
    • client.Purchases.Pending()
    • client.Purchases.Capture()
    • client.Purchases.Cancel()
  • Modified the PurchaseSubscription struct:
    • Added ShippingAddress
    • Added ShippingAddressID
    • Added ShippingMethodCode
    • Added ShippingAmountInCents

Redemptions

  • Renamed client.Redemptions.GetForAccount to client.Redemptions.ListForAccount()
  • Renamed client.Redemptions.GetForInvoice() to client.Redemptions.ListForInvoice()
  • Added the following methods:
    • client.Redemptions.ListSubscription()
  • client.Redemptions.Redeem() now accepts a CouponRedemption instead of individual parameters for accountCode and currency.
  • client.Redemptions.RedeemToSubscription() has been removed. Use Redeem() and set the SubscriptionUUID field in the CouponRedemption struct.

Shipping Addresses

  • Removed client.ShippingAddresses.GetSubscriptions(). This method does not exist in the Recurly API per their documentation.

Shipping Methods

New API added for v2.20.

Subscriptions

  • in client.Subscriptions: TerminateWithPartialRefund(), TerminateWithFullRefund() and TerminateWithoutRefund() have all been consolidated into client.Subscriptions.Terminate(). The refundType parameter accepts partial, full, or none respectively.
  • In the Subscription struct:
    • Removed the MakeUpdate() method
  • In the NewSubscription struct:
    • Changed UnitAmountInCents from int to NullInt
    • Added RevenueScheduleType
    • Added ShippingAddress
    • Added ShippingAddressID
    • Added ImportedTrial
    • Added ShippingMethodCode
    • Added ShippingAmountInCents
  • In the UpdateSubscription struct:
    • Changed UnitAmountInCents from int to NullInt
    • Changed AutoRenew from bool to NullBool
    • Added ImportedTrial
    • Added RevenueScheduleType

Transactions

  • Removed client.Transactions.Create() (deprecated per Recurly). Use the Purchases API instead.
  • Removed the following helper methods from CVVResult
    • IsMatch()
    • IsNoMatch()
    • NotProcessed()
    • ShouldHaveBeenPresent()
    • UnableToProcess()
  • Removed TransactionResult embedded type from CVVResult and AVSResult. All previous fields are accessible directly on the struct now.

Mock

  • Changed NewClient
    • Previous: func NewClient(httpClient *http.Client) *recurly.Client
    • New: func NewClient(subdomain, apiKey string) *Client
  • Added examples and Godoc documentation for testing the package using mocks

If you run into any issues with migrating to v1, or find any issues in this documentation, please open an issue.