From 3601f1468580f40ffd59879dd8fbb759c3e97fb0 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:46:55 -0700 Subject: [PATCH 1/3] feat(fedex): add FedEx Multi-Factor Authentication endpoints Co-Authored-By: Claude Sonnet 4.5 --- fedex_registration.go | 159 +++++++++++++++++++++++++++++++++++++ fedex_registration_test.go | 156 ++++++++++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 fedex_registration.go create mode 100644 fedex_registration_test.go diff --git a/fedex_registration.go b/fedex_registration.go new file mode 100644 index 0000000..e9e998b --- /dev/null +++ b/fedex_registration.go @@ -0,0 +1,159 @@ +package easypost + +import ( + "context" + "fmt" + "net/http" + + "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 := uuid.New().String() + uuidStr = uuidStr[:8] + uuidStr[9:13] + uuidStr[14:18] + uuidStr[19:23] + uuidStr[24:] + m["name"] = uuidStr + } +} diff --git a/fedex_registration_test.go b/fedex_registration_test.go new file mode 100644 index 0000000..dfff79c --- /dev/null +++ b/fedex_registration_test.go @@ -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.Nil(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"]) +} From a5665963341c1082c308c7d659c09a19f03a28ec Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:28:30 -0700 Subject: [PATCH 2/3] chore: cleanup --- fedex_registration.go | 8 ++++---- fedex_registration_test.go | 2 +- justfile | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/fedex_registration.go b/fedex_registration.go index e9e998b..fe52684 100644 --- a/fedex_registration.go +++ b/fedex_registration.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "strings" "github.com/google/uuid" ) @@ -11,9 +12,9 @@ import ( // 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"` + 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"` + 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"` @@ -152,8 +153,7 @@ func wrapInvoiceValidation(params map[string]interface{}) map[string]interface{} // 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 := uuid.New().String() - uuidStr = uuidStr[:8] + uuidStr[9:13] + uuidStr[14:18] + uuidStr[19:23] + uuidStr[24:] + uuidStr := strings.ReplaceAll(uuid.New().String(), "-", "") m["name"] = uuidStr } } diff --git a/fedex_registration_test.go b/fedex_registration_test.go index dfff79c..2b95488 100644 --- a/fedex_registration_test.go +++ b/fedex_registration_test.go @@ -76,7 +76,7 @@ func (c *ClientTests) TestRegisterFedExAddress() { assert.Contains(response.Options, "SMS") assert.Contains(response.Options, "CALL") assert.Contains(response.Options, "INVOICE") - assert.Equal("***-***-9721", *response.PhoneNumber) + assert.Equal("***-***-9721", response.PhoneNumber) } func (c *ClientTests) TestRequestFedExPin() { diff --git a/justfile b/justfile index 1265af2..7d13c0e 100644 --- a/justfile +++ b/justfile @@ -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: @@ -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 From 57c057d1f49a0379c4cdbc87a237a1946b6068f0 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:31:53 -0700 Subject: [PATCH 3/3] fix: test --- fedex_registration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedex_registration_test.go b/fedex_registration_test.go index 2b95488..23fdeb1 100644 --- a/fedex_registration_test.go +++ b/fedex_registration_test.go @@ -72,7 +72,7 @@ func (c *ClientTests) TestRegisterFedExAddress() { response, err := client.RegisterFedExAddress(fedexAccountNumber, params) require.NoError(err) - assert.Nil(response.EmailAddress) + assert.Equal("", response.EmailAddress) assert.Contains(response.Options, "SMS") assert.Contains(response.Options, "CALL") assert.Contains(response.Options, "INVOICE")