Skip to content

Commit

Permalink
feat: Account for new UPS endpoint in carrier account create/update f…
Browse files Browse the repository at this point in the history
…unctions (#224)

- Add init-examples-submodule Make step
- New CreateUpsCarrierAccount and UpdateUpsCarrierAccount functions
- New UPS-specific create and update parameter structs
- New InvalidFunctionError, raised if users try to mix UPS- and non-UPS functionality
- New/updated unit tests, cassettes as needed
- Docstring cleanup and clarifications
  • Loading branch information
nwithan8 authored Jul 12, 2024
1 parent c77c5e4 commit 71c14fe
Show file tree
Hide file tree
Showing 11 changed files with 727 additions and 102 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## Next Release

- New `CreateUpsCarrierAccount` and `UpdateUpsCarrierAccount` methods and associated parameter structs, required to use for UPS accounts due to new `/ups_oauth_registrations` endpoint.
- Starting `2024-08-05`, UPS accounts will require a new payload to register or update. See [UPS OAuth 2.0 Update](https://support.easypost.com/hc/en-us/articles/26635027512717-UPS-OAuth-2-0-Update) for more details.
- Attempting to use the generic `CreateCarrierAccount` and `UpdateCarrierAccount` methods with UPS accounts will throw an `InvalidFunctionError`.

## v4.3.1 (2024-07-01)

- Adds missing `Readable` and `Logo` fields to `CarrierType` struct
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ clean:
coverage:
go clean -testcache && go test ./tests -v -coverprofile=covprofile -coverpkg=./... && go tool cover -html=covprofile

## init-examples-submodule - Initialize the examples submodule
init-examples-submodule:
git submodule init
git submodule update

## install - Install and vendor dependencies
install: | update-examples-submodule
install: | init-examples-submodule
brew install golangci-lint || exit 0
go mod vendor
go build -o $(PROJECT_PATH)
Expand Down
167 changes: 130 additions & 37 deletions carrier.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ type CarrierField struct {
Value string `json:"value,omitempty"`
}

// CarrierFields contains the data for carrier account fields for production
// and/or test credentials.
// CarrierFields contains the data for carrier account fields for production and/or test credentials.
type CarrierFields struct {
Credentials map[string]*CarrierField `json:"credentials,omitempty"`
TestCredentials map[string]*CarrierField `json:"test_credentials,omitempty"`
AutoLink bool `json:"auto_link,omitempty"`
CustomWorkflow bool `json:"custom_workflow,omitempty"`
}

// CarrierAccount encapsulates credentials and other information related to a
// carrier account.
// CarrierAccount encapsulates credentials and other information related to a carrier account.
// This struct is also used as a parameter set for creating and updating non-UPS carrier accounts.
type CarrierAccount struct {
ID string `json:"id,omitempty"`
Object string `json:"object,omitempty"`
Expand All @@ -39,8 +38,7 @@ type CarrierAccount struct {
BillingType string `json:"billing_type,omitempty"`
}

// CarrierType contains information on a supported carrier. It can be used to
// determine the valid fields for a carrier account.
// CarrierType contains information on a supported carrier. It can be used to determine the valid fields for a carrier account.
type CarrierType struct {
Object string `json:"object,omitempty"`
Type string `json:"type,omitempty"`
Expand All @@ -50,34 +48,58 @@ type CarrierType struct {
}

type carrierAccountRequest struct {
CarrierAccount *CarrierAccount `json:"carrier_account,omitempty"`
Data *CarrierAccount `json:"carrier_account,omitempty"`
}

func (c *Client) selectCarrierAccountCreationEndpoint(account CarrierAccount) string {
// UpsCarrierAccountCreationParameters contains the parameters needed to create a new UPS carrier account.
type UpsCarrierAccountCreationParameters struct {
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
Reference string `json:"reference,omitempty"`
AccountNumber string `json:"account_number,omitempty"`
}

type upsCarrierAccountCreationRequest struct {
Data *UpsCarrierAccountCreationParameters `json:"ups_oauth_registrations,omitempty"`
}

// UpsCarrierAccountUpdateParameters contains the parameters needed to update a UPS carrier account.
type UpsCarrierAccountUpdateParameters struct {
AccountNumber string `json:"account_number,omitempty"`
}

type upsCarrierAccountUpdateRequest struct {
Data *UpsCarrierAccountUpdateParameters `json:"ups_oauth_registrations,omitempty"`
}

func (c *Client) selectCarrierAccountCreationEndpoint(typ string) string {
for _, carrier := range getCarrierAccountTypesWithCustomWorkflows() {
if account.Type == carrier {
if typ == carrier {
return "carrier_accounts/register"
}
}

for _, carrier := range getUpsCarrierAccountTypes() {
if typ == carrier {
return "ups_oauth_registrations"
}
}

return "carrier_accounts"
}

// GetCarrierTypes returns a list of supported carrier types for the current
// user.
// GetCarrierTypes returns a list of supported carrier types for the current user.
func (c *Client) GetCarrierTypes() (out []*CarrierType, err error) {
return c.GetCarrierTypesWithContext(context.Background())
}

// GetCarrierTypesWithContext performs the same operation as GetCarrierTypes,
// but allows specifying a context that can interrupt the request.
// GetCarrierTypesWithContext performs the same operation as GetCarrierTypes, but allows specifying a context that can interrupt the request.
func (c *Client) GetCarrierTypesWithContext(ctx context.Context) (out []*CarrierType, err error) {
err = c.get(ctx, "carrier_types", &out)
return
}

// CreateCarrierAccount creates a new carrier account. It can only be used with
// a production API key.
// CreateCarrierAccount creates a new carrier account. It can only be used with a production API key.
//
// c := easypost.New(MyEasyPostAPIKey)
// out, err := c.CreateCarrierAccount(
Expand All @@ -92,29 +114,60 @@ func (c *Client) GetCarrierTypesWithContext(ctx context.Context) (out []*Carrier
// },
// },
// )
//
// Users cannot create UPS accounts with this function, must use CreateUpsCarrierAccount. An error will be returned if the user tries to create a UPS account with this function.
func (c *Client) CreateCarrierAccount(in *CarrierAccount) (out *CarrierAccount, err error) {
return c.CreateCarrierAccountWithContext(context.Background(), in)
}

// CreateCarrierAccountWithContext performs the same operation as
// CreateCarrierAccount, but allows specifying a context that can interrupt the
// request.
// CreateCarrierAccountWithContext performs the same operation as CreateCarrierAccount, but allows specifying a context that can interrupt the request.
// Users cannot create UPS accounts with this function, must use CreateUpsCarrierAccount. An error will be returned if the user tries to create a UPS account with this function.
func (c *Client) CreateCarrierAccountWithContext(ctx context.Context, in *CarrierAccount) (out *CarrierAccount, err error) {
req := &carrierAccountRequest{CarrierAccount: in}
endpoint := c.selectCarrierAccountCreationEndpoint(*in)
// Users cannot create UPS accounts with this function, must use CreateUpsCarrierAccount
for _, carrier := range getUpsCarrierAccountTypes() {
if in.Type == carrier {
return nil, newInvalidFunctionError("users must use CreateUpsCarrierAccount to create UPS accounts")
}
}

req := &carrierAccountRequest{Data: in}
endpoint := c.selectCarrierAccountCreationEndpoint(in.Type)
err = c.post(ctx, endpoint, req, &out)
return
}

// ListCarrierAccounts returns a list of all carrier accounts available to the
// authenticated account.
// CreateUpsCarrierAccount creates a new UPS carrier account. It can only be used with a production API key.
// Users cannot create non-UPS accounts with this function, must use CreateCarrierAccount. An error will be returned if the user tries to create a non-UPS account with this function.
func (c *Client) CreateUpsCarrierAccount(in *UpsCarrierAccountCreationParameters) (out *CarrierAccount, err error) {
return c.CreateUpsCarrierAccountWithContext(context.Background(), in)
}

// CreateUpsCarrierAccountWithContext performs the same operation as CreateUpsCarrierAccount, but allows specifying a context that can interrupt the request.
// Users cannot create non-UPS accounts with this function, must use CreateCarrierAccount. An error will be returned if the user tries to create a non-UPS account with this function.
func (c *Client) CreateUpsCarrierAccountWithContext(ctx context.Context, in *UpsCarrierAccountCreationParameters) (out *CarrierAccount, err error) {
// Users cannot create non-UPS accounts with this function, must use CreateCarrierAccount
isUpsAccount := false
for _, carrier := range getUpsCarrierAccountTypes() {
if in.Type == carrier {
isUpsAccount = true
break
}
}
if !isUpsAccount {
return nil, newInvalidFunctionError("users must use CreateCarrierAccount to create non-UPS accounts")
}

req := &upsCarrierAccountCreationRequest{Data: in}
err = c.post(ctx, "ups_oauth_registrations", req, &out)
return
}

// ListCarrierAccounts returns a list of all carrier accounts available to the authenticated account.
func (c *Client) ListCarrierAccounts() (out []*CarrierAccount, err error) {
return c.ListCarrierAccountsWithContext(context.Background())
}

// ListCarrierAccountsWithContext performs the same operation as
// ListCarrierAccounts, but allows specifying a context that can interrupt the
// request.
// ListCarrierAccountsWithContext performs the same operation as ListCarrierAccounts, but allows specifying a context that can interrupt the request.
func (c *Client) ListCarrierAccountsWithContext(ctx context.Context) (out []*CarrierAccount, err error) {
err = c.get(ctx, "carrier_accounts", &out)
return
Expand All @@ -125,16 +178,13 @@ func (c *Client) GetCarrierAccount(carrierAccountID string) (out *CarrierAccount
return c.GetCarrierAccountWithContext(context.Background(), carrierAccountID)
}

// GetCarrierAccountWithContext performs the same operation as
// GetCarrierAccount, but allows specifying a context that can interrupt the
// request.
// GetCarrierAccountWithContext performs the same operation as GetCarrierAccount, but allows specifying a context that can interrupt the request.
func (c *Client) GetCarrierAccountWithContext(ctx context.Context, carrierAccountID string) (out *CarrierAccount, err error) {
err = c.get(ctx, "carrier_accounts/"+carrierAccountID, &out)
return
}

// UpdateCarrierAccount updates the carrier account. Only the Description,
// Reference, Credentials and TestCredentials attributes can be updated.
// UpdateCarrierAccount updates the carrier account. Only the Description, Reference, Credentials and TestCredentials attributes can be updated.
//
// c := easypost.New(MyEasyPostAPIKey)
// out, err := c.UpdateCarrierAccount(
Expand All @@ -146,27 +196,70 @@ func (c *Client) GetCarrierAccountWithContext(ctx context.Context, carrierAccoun
// },
// },
// )
//
// Users cannot update UPS accounts with this function, must use UpdateUpsCarrierAccount. An error will be returned if the user tries to update a UPS account with this function.
func (c *Client) UpdateCarrierAccount(in *CarrierAccount) (out *CarrierAccount, err error) {
return c.UpdateCarrierAccountWithContext(context.Background(), in)
}

// UpdateCarrierAccountWithContext performs the same operation as
// UpdateCarrierAccount, but allows specifying a context that can interrupt the
// request.
// UpdateCarrierAccountWithContext performs the same operation as UpdateCarrierAccount, but allows specifying a context that can interrupt the request.
// Users cannot update UPS accounts with this function, must use UpdateUpsCarrierAccount. An error will be returned if the user tries to update a UPS account with this function.
func (c *Client) UpdateCarrierAccountWithContext(ctx context.Context, in *CarrierAccount) (out *CarrierAccount, err error) {
req := &carrierAccountRequest{CarrierAccount: in}
account, err := c.GetCarrierAccount(in.ID)
if err != nil {
return nil, err
}

// Users cannot update UPS accounts with this function, must use UpdateUpsCarrierAccount
for _, carrier := range getUpsCarrierAccountTypes() {
if account.Type == carrier {
return nil, newInvalidFunctionError("users must use UpdateUpsCarrierAccount to update UPS accounts")
}
}

req := &carrierAccountRequest{Data: in}
err = c.patch(ctx, "carrier_accounts/"+in.ID, req, &out)
return
}

// UpdateUpsCarrierAccount updates a UPS carrier account.
// Users cannot update non-UPS accounts with this function, must use UpdateCarrierAccount. An error will be returned if the user tries to update a non-UPS account with this function.
func (c *Client) UpdateUpsCarrierAccount(id string, in *UpsCarrierAccountUpdateParameters) (out *CarrierAccount, err error) {
return c.UpdateUpsCarrierAccountWithContext(context.Background(), id, in)
}

// UpdateUpsCarrierAccountWithContext performs the same operation as UpdateUpsCarrierAccount, but allows specifying a context that can interrupt the request.
// Users cannot update non-UPS accounts with this function, must use UpdateCarrierAccount. An error will be returned if the user tries to update a non-UPS account with this function.
func (c *Client) UpdateUpsCarrierAccountWithContext(ctx context.Context, id string, in *UpsCarrierAccountUpdateParameters) (out *CarrierAccount, err error) {
account, err := c.GetCarrierAccount(id)
if err != nil {
return nil, err
}

// Users cannot update non-UPS accounts with this function, must use UpdateCarrierAccount
isUpsAccount := false
for _, carrier := range getUpsCarrierAccountTypes() {
if account.Type == carrier {
isUpsAccount = true
break
}
}
if !isUpsAccount {
return nil, newInvalidFunctionError("users must use UpdateCarrierAccount to update non-UPS accounts")
}

req := &upsCarrierAccountUpdateRequest{Data: in}
err = c.patch(ctx, "ups_oauth_registrations/"+id, req, &out)
return

}

// DeleteCarrierAccount removes the carrier account with the given ID.
func (c *Client) DeleteCarrierAccount(carrierAccountID string) error {
return c.DeleteCarrierAccountWithContext(context.Background(), carrierAccountID)
}

// DeleteCarrierAccountWithContext performs the same operation as
// DeleteCarrierAccount, but allows specifying a context that can interrupt the
// request.
// DeleteCarrierAccountWithContext performs the same operation as DeleteCarrierAccount, but allows specifying a context that can interrupt the request.
func (c *Client) DeleteCarrierAccountWithContext(ctx context.Context, carrierAccountID string) error {
return c.del(ctx, "carrier_accounts/"+carrierAccountID)
}
6 changes: 5 additions & 1 deletion constants.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package easypost

func getCarrierAccountTypesWithCustomWorkflows() []string {
return []string{"FedexAccount", "UpsAccount", "FedexSmartpostAccount"}
return []string{"FedexAccount", "FedexSmartpostAccount"}
}

func getUpsCarrierAccountTypes() []string {
return []string{"UpsAccount", "UpsMailInnovationsAccount", "UpsSurepostAccount"}
}

var ApiDidNotReturnErrorDetails = "API did not return error details"
Expand Down
10 changes: 10 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ func newExternalApiError(message string) *ExternalApiError {
return &ExternalApiError{LibraryError{Message: message}}
}

// InvalidFunctionError is raised when a function call is invalid or not allowed.
type InvalidFunctionError struct {
LocalError
}

// newInvalidFunctionError returns a new InvalidFunctionError object with the given message.
func newInvalidFunctionError(message string) *InvalidFunctionError {
return &InvalidFunctionError{LocalError{LibraryError{Message: message}}}
}

// API/HTTP error types

// APIError represents an error that occurred while communicating with the EasyPost API.
Expand Down
Loading

0 comments on commit 71c14fe

Please sign in to comment.