Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions fedex_registration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package easypost

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/google/uuid"
)

// FedExAccountValidationResponse represents the response from FedEx account validation endpoints.
type FedExAccountValidationResponse struct {
// If the response contains the following, one must complete pin or invoice validation next
EmailAddress string `json:"email_address,omitempty" url:"email_address,omitempty"`
Options []string `json:"options,omitempty" url:"options,omitempty"`
PhoneNumber string `json:"phone_number,omitempty" url:"phone_number,omitempty"`

// If the response contains the following, pre-validation has been completed
ID string `json:"id,omitempty" url:"id,omitempty"`
Object string `json:"object,omitempty" url:"object,omitempty"`
Type string `json:"type,omitempty" url:"type,omitempty"`
Credentials map[string]string `json:"credentials,omitempty" url:"credentials,omitempty"`
}

// FedExRequestPinResponse represents the response from requesting a PIN.
type FedExRequestPinResponse struct {
Message string `json:"message,omitempty" url:"message,omitempty"`
}

// RegisterFedExAddress registers the billing address for a FedEx account.
// Advanced method for custom parameter structures.
func (c *Client) RegisterFedExAddress(fedexAccountNumber string, params map[string]interface{}) (out *FedExAccountValidationResponse, err error) {
return c.RegisterFedExAddressWithContext(context.Background(), fedexAccountNumber, params)
}

// RegisterFedExAddressWithContext performs the same operation as RegisterFedExAddress, but allows specifying a context that can interrupt the request.
func (c *Client) RegisterFedExAddressWithContext(ctx context.Context, fedexAccountNumber string, params map[string]interface{}) (out *FedExAccountValidationResponse, err error) {
wrappedParams := wrapAddressValidation(params)
endpoint := fmt.Sprintf("fedex_registrations/%s/address", fedexAccountNumber)
err = c.do(ctx, http.MethodPost, endpoint, wrappedParams, &out)
return
}

// RequestFedExPin requests a PIN for FedEx account verification.
func (c *Client) RequestFedExPin(fedexAccountNumber string, pinMethodOption string) (out *FedExRequestPinResponse, err error) {
return c.RequestFedExPinWithContext(context.Background(), fedexAccountNumber, pinMethodOption)
}

// RequestFedExPinWithContext performs the same operation as RequestFedExPin, but allows specifying a context that can interrupt the request.
func (c *Client) RequestFedExPinWithContext(ctx context.Context, fedexAccountNumber string, pinMethodOption string) (out *FedExRequestPinResponse, err error) {
wrappedParams := map[string]interface{}{
"pin_method": map[string]interface{}{
"option": pinMethodOption,
},
}
endpoint := fmt.Sprintf("fedex_registrations/%s/pin", fedexAccountNumber)
err = c.do(ctx, http.MethodPost, endpoint, wrappedParams, &out)
return
}

// ValidateFedExPin validates the PIN entered by the user for FedEx account verification.
func (c *Client) ValidateFedExPin(fedexAccountNumber string, params map[string]interface{}) (out *FedExAccountValidationResponse, err error) {
return c.ValidateFedExPinWithContext(context.Background(), fedexAccountNumber, params)
}

// ValidateFedExPinWithContext performs the same operation as ValidateFedExPin, but allows specifying a context that can interrupt the request.
func (c *Client) ValidateFedExPinWithContext(ctx context.Context, fedexAccountNumber string, params map[string]interface{}) (out *FedExAccountValidationResponse, err error) {
wrappedParams := wrapPinValidation(params)
endpoint := fmt.Sprintf("fedex_registrations/%s/pin/validate", fedexAccountNumber)
err = c.do(ctx, http.MethodPost, endpoint, wrappedParams, &out)
return
}

// SubmitFedExInvoice submits invoice information to complete FedEx account registration.
func (c *Client) SubmitFedExInvoice(fedexAccountNumber string, params map[string]interface{}) (out *FedExAccountValidationResponse, err error) {
return c.SubmitFedExInvoiceWithContext(context.Background(), fedexAccountNumber, params)
}

// SubmitFedExInvoiceWithContext performs the same operation as SubmitFedExInvoice, but allows specifying a context that can interrupt the request.
func (c *Client) SubmitFedExInvoiceWithContext(ctx context.Context, fedexAccountNumber string, params map[string]interface{}) (out *FedExAccountValidationResponse, err error) {
wrappedParams := wrapInvoiceValidation(params)
endpoint := fmt.Sprintf("fedex_registrations/%s/invoice", fedexAccountNumber)
err = c.do(ctx, http.MethodPost, endpoint, wrappedParams, &out)
return
}

// wrapAddressValidation wraps address validation parameters and ensures the "name" field exists.
// If not present, generates a UUID (with hyphens removed) as the name.
func wrapAddressValidation(params map[string]interface{}) map[string]interface{} {
wrappedParams := make(map[string]interface{})

if addressValidation, ok := params["address_validation"].(map[string]interface{}); ok {
addressValidationCopy := make(map[string]interface{})
for k, v := range addressValidation {
addressValidationCopy[k] = v
}
ensureNameField(addressValidationCopy)
wrappedParams["address_validation"] = addressValidationCopy
}

if easypostDetails, ok := params["easypost_details"]; ok {
wrappedParams["easypost_details"] = easypostDetails
}

return wrappedParams
}

// wrapPinValidation wraps PIN validation parameters and ensures the "name" field exists.
// If not present, generates a UUID (with hyphens removed) as the name.
func wrapPinValidation(params map[string]interface{}) map[string]interface{} {
wrappedParams := make(map[string]interface{})

if pinValidation, ok := params["pin_validation"].(map[string]interface{}); ok {
pinValidationCopy := make(map[string]interface{})
for k, v := range pinValidation {
pinValidationCopy[k] = v
}
ensureNameField(pinValidationCopy)
wrappedParams["pin_validation"] = pinValidationCopy
}

if easypostDetails, ok := params["easypost_details"]; ok {
wrappedParams["easypost_details"] = easypostDetails
}

return wrappedParams
}

// wrapInvoiceValidation wraps invoice validation parameters and ensures the "name" field exists.
// If not present, generates a UUID (with hyphens removed) as the name.
func wrapInvoiceValidation(params map[string]interface{}) map[string]interface{} {
wrappedParams := make(map[string]interface{})

if invoiceValidation, ok := params["invoice_validation"].(map[string]interface{}); ok {
invoiceValidationCopy := make(map[string]interface{})
for k, v := range invoiceValidation {
invoiceValidationCopy[k] = v
}
ensureNameField(invoiceValidationCopy)
wrappedParams["invoice_validation"] = invoiceValidationCopy
}

if easypostDetails, ok := params["easypost_details"]; ok {
wrappedParams["easypost_details"] = easypostDetails
}

return wrappedParams
}

// ensureNameField ensures the "name" field exists in the provided map.
// If not present, generates a UUID (with hyphens removed) as the name.
// This follows the pattern used in the web UI implementation.
func ensureNameField(m map[string]interface{}) {
if _, exists := m["name"]; !exists || m["name"] == nil {
uuidStr := strings.ReplaceAll(uuid.New().String(), "-", "")
m["name"] = uuidStr
}
}
156 changes: 156 additions & 0 deletions fedex_registration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package easypost

func GetFedExRegistrationMockRequests() []MockRequest {
return []MockRequest{
{
MatchRule: MockRequestMatchRule{
Method: "POST",
UrlRegexPattern: "v2\\/fedex_registrations\\/\\S*\\/address$",
},
ResponseInfo: MockRequestResponseInfo{
StatusCode: 200,
Body: `{"email_address":null,"options":["SMS","CALL","INVOICE"],"phone_number":"***-***-9721"}`,
},
},
{
MatchRule: MockRequestMatchRule{
Method: "POST",
UrlRegexPattern: "v2\\/fedex_registrations\\/\\S*\\/pin$",
},
ResponseInfo: MockRequestResponseInfo{
StatusCode: 200,
Body: `{"message":"sent secured Pin"}`,
},
},
{
MatchRule: MockRequestMatchRule{
Method: "POST",
UrlRegexPattern: "v2\\/fedex_registrations\\/\\S*\\/pin\\/validate$",
},
ResponseInfo: MockRequestResponseInfo{
StatusCode: 200,
Body: `{"id":"ca_123","object":"CarrierAccount","type":"FedexAccount","credentials":{"account_number":"123456789","mfa_key":"123456789-XXXXX"}}`,
},
},
{
MatchRule: MockRequestMatchRule{
Method: "POST",
UrlRegexPattern: "v2\\/fedex_registrations\\/\\S*\\/invoice$",
},
ResponseInfo: MockRequestResponseInfo{
StatusCode: 200,
Body: `{"id":"ca_123","object":"CarrierAccount","type":"FedexAccount","credentials":{"account_number":"123456789","mfa_key":"123456789-XXXXX"}}`,
},
},
}
}

func (c *ClientTests) TestRegisterFedExAddress() {
mockRequests := GetFedExRegistrationMockRequests()
client := c.MockClient(mockRequests)
assert, require := c.Assert(), c.Require()

fedexAccountNumber := "123456789"
addressValidation := map[string]interface{}{
"name": "BILLING NAME",
"street1": "1234 BILLING STREET",
"city": "BILLINGCITY",
"state": "ST",
"postal_code": "12345",
"country_code": "US",
}

easypostDetails := map[string]interface{}{
"carrier_account_id": "ca_123",
}

params := map[string]interface{}{
"address_validation": addressValidation,
"easypost_details": easypostDetails,
}

response, err := client.RegisterFedExAddress(fedexAccountNumber, params)
require.NoError(err)

assert.Equal("", response.EmailAddress)
assert.Contains(response.Options, "SMS")
assert.Contains(response.Options, "CALL")
assert.Contains(response.Options, "INVOICE")
assert.Equal("***-***-9721", response.PhoneNumber)
}

func (c *ClientTests) TestRequestFedExPin() {
mockRequests := GetFedExRegistrationMockRequests()
client := c.MockClient(mockRequests)
assert, require := c.Assert(), c.Require()

fedexAccountNumber := "123456789"

response, err := client.RequestFedExPin(fedexAccountNumber, "SMS")
require.NoError(err)

assert.Equal("sent secured Pin", response.Message)
}

func (c *ClientTests) TestValidateFedExPin() {
mockRequests := GetFedExRegistrationMockRequests()
client := c.MockClient(mockRequests)
assert, require := c.Assert(), c.Require()

fedexAccountNumber := "123456789"
pinValidation := map[string]interface{}{
"pin_code": "123456",
"name": "BILLING NAME",
}

easypostDetails := map[string]interface{}{
"carrier_account_id": "ca_123",
}

params := map[string]interface{}{
"pin_validation": pinValidation,
"easypost_details": easypostDetails,
}

response, err := client.ValidateFedExPin(fedexAccountNumber, params)
require.NoError(err)

assert.Equal("ca_123", response.ID)
assert.Equal("CarrierAccount", response.Object)
assert.Equal("FedexAccount", response.Type)
assert.Equal("123456789", response.Credentials["account_number"])
assert.Equal("123456789-XXXXX", response.Credentials["mfa_key"])
}

func (c *ClientTests) TestSubmitFedExInvoice() {
mockRequests := GetFedExRegistrationMockRequests()
client := c.MockClient(mockRequests)
assert, require := c.Assert(), c.Require()

fedexAccountNumber := "123456789"
invoiceValidation := map[string]interface{}{
"name": "BILLING NAME",
"invoice_number": "INV-12345",
"invoice_date": "2025-12-08",
"invoice_amount": "100.00",
"invoice_currency": "USD",
}

easypostDetails := map[string]interface{}{
"carrier_account_id": "ca_123",
}

params := map[string]interface{}{
"invoice_validation": invoiceValidation,
"easypost_details": easypostDetails,
}

response, err := client.SubmitFedExInvoice(fedexAccountNumber, params)
require.NoError(err)

assert.Equal("ca_123", response.ID)
assert.Equal("CarrierAccount", response.Object)
assert.Equal("FedexAccount", response.Type)
assert.Equal("123456789", response.Credentials["account_number"])
assert.Equal("123456789-XXXXX", response.Credentials["mfa_key"])
}
4 changes: 2 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build:
# Clean the project
clean:
rm -rf dist
rm $(go env GOPATH)/bin/easypost-go
rm -f $(go env GOPATH)/bin/easypost-go

# Get test coverage and open it in a browser
coverage:
Expand All @@ -22,7 +22,7 @@ init-examples-submodule:
# Install and vendor dependencies
install: init-examples-submodule
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.6
curl -sSfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
curl -sSfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.22.11
go mod vendor

# Lint the project
Expand Down