Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: bold-commerce/go-shopify
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: graze/go-shopify
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.
  • 17 commits
  • 10 files changed
  • 2 contributors

Commits on Dec 18, 2022

  1. Copy the full SHA
    f475a50 View commit details
  2. Handle omitted Properties

    brendankay committed Dec 18, 2022
    Copy the full SHA
    36cee29 View commit details

Commits on Dec 19, 2022

  1. Remove DestinationLocation

    brendankay committed Dec 19, 2022
    Copy the full SHA
    ee2b1c5 View commit details
  2. Remove OriginLocation

    brendankay committed Dec 19, 2022
    Copy the full SHA
    f7484c3 View commit details

Commits on Dec 20, 2022

  1. Copy the full SHA
    4b37c17 View commit details

Commits on Dec 27, 2022

  1. List fulfillment orders

    brendankay committed Dec 27, 2022
    Copy the full SHA
    4b6756d View commit details
  2. Copy the full SHA
    6347598 View commit details

Commits on Jan 1, 2023

  1. Copy the full SHA
    f4bd6b4 View commit details
  2. Formatting

    brendankay committed Jan 1, 2023
    Copy the full SHA
    4f1e0df View commit details

Commits on Jan 16, 2023

  1. Copy the full SHA
    0f5c591 View commit details
  2. Merge pull request #2 from graze/add-body-to-error

    Add response body to error to try and figure out Unknown Error
    brendankay authored Jan 16, 2023
    Copy the full SHA
    04710c0 View commit details

Commits on Jan 17, 2023

  1. Copy the full SHA
    ffeef15 View commit details
  2. Merge pull request #3 from graze/add-request-response-to-error

    Add request and response to error
    brendankay authored Jan 17, 2023
    Copy the full SHA
    94d22e1 View commit details
  3. Copy the full SHA
    afd078a View commit details
  4. Merge pull request #4 from graze/add-request-response-to-error

    Add request and response to error
    brendankay authored Jan 17, 2023
    Copy the full SHA
    35c3cac View commit details

Commits on May 16, 2023

  1. Copy the full SHA
    1dbcfdb View commit details
  2. Merge pull request #5 from graze/add-fulfilment-order-line-items

    Add FulfillmentOrder.LineItems
    brendankay authored May 16, 2023
    Copy the full SHA
    2a9a0ac View commit details
Showing with 233 additions and 127 deletions.
  1. +0 −36 fixtures/orderlineitems/valid.json
  2. +28 −1 fulfillment.go
  3. +52 −0 fulfillment_order.go
  4. +29 −0 fulfillment_order_test.go
  5. +14 −1 fulfillment_test.go
  6. +24 −10 goshopify.go
  7. +21 −13 order.go
  8. +32 −57 order_test.go
  9. +15 −9 util.go
  10. +18 −0 util_test.go
36 changes: 0 additions & 36 deletions fixtures/orderlineitems/valid.json
Original file line number Diff line number Diff line change
@@ -42,42 +42,6 @@
"variant_inventory_management": "shopify",
"variant_title": "Test Variant",
"vendor": "Test Vendor",
"origin_location": {
"id": 123,
"address1": "100 some street",
"address2": "",
"city": "Winnipeg",
"company": "Acme Corporation",
"country": "Canada",
"country_code": "CA",
"first_name": "Bob",
"last_name": "Smith",
"latitude": 49.811550,
"longitude": -97.189480,
"name": "test address",
"phone": "8675309",
"province": "Manitoba",
"province_code": "MB",
"zip": "R3Y 0L6"
},
"destination_location": {
"id": 124,
"address1": "200 some street",
"address2": "",
"city": "Winnipeg",
"company": "Acme Corporation",
"country": "Canada",
"country_code": "CA",
"first_name": "Bob",
"last_name": "Smith",
"latitude": 49.811550,
"longitude": -97.189480,
"name": "test address",
"phone": "8675309",
"province": "Manitoba",
"province_code": "MB",
"zip": "R3Y 0L6"
},
"applied_discount": {
"applied_discount": "test discount",
"description": "my test discount",
29 changes: 28 additions & 1 deletion fulfillment.go
Original file line number Diff line number Diff line change
@@ -59,6 +59,32 @@ type Fulfillment struct {
Receipt Receipt `json:"receipt,omitempty"`
LineItems []LineItem `json:"line_items,omitempty"`
NotifyCustomer bool `json:"notify_customer"`

// Properties used when creating an fulfllment via the fulfillments endpoint in version 2022-07
LineItemsByFulfillmentOrder []LineItemByFulfillmentOrder `json:"line_items_by_fulfillment_order,omitempty"`
TrackingInfo TrackingInfo `json:"tracking_info,omitempty"`
}

type TrackingInfo struct {
Company string `json:"company,omitempty"`
Number string `json:"number,omitempty"`
Url string `json:"url,omitempty"`
}

type LineItemByFulfillmentOrder struct {
FulfillmentOrderID int64 `json:"fulfillment_order_id"`
FulfillmentOrderLineItems []FulfillmentOrderLineItem `json:"fulfillment_order_line_items,omitempty"`
}

type FulfillmentOrderLineItem struct {
ID int64 `json:"id,omitempty"`
ShopID int64 `json:"shop_id,omitempty"`
FulfillmentOrderID int64 `json:"fulfillment_order_id,omitempty"`
Quantity int `json:"quantity,omitempty"`
LineItemID int64 `json:"line_item_id,omitempty"`
InventoryItemID int64 `json:"inventory_item_id,omitempty"`
FulfillableQuantity int `json:"fulfillable_quantity,omitempty"`
VariantID int64 `json:"variant_id,omitempty"`
}

// Receipt represents a Shopify receipt.
@@ -104,7 +130,8 @@ func (s *FulfillmentServiceOp) Get(fulfillmentID int64, options interface{}) (*F

// Create a new fulfillment
func (s *FulfillmentServiceOp) Create(fulfillment Fulfillment) (*Fulfillment, error) {
prefix := FulfillmentPathPrefix(s.resource, s.resourceID)
// As of 2022-07 fulfillments need to be created using /fulfillments.json, not /orders/{order_id}/fulfillments.json
prefix := FulfillmentPathPrefix("", 0)
path := fmt.Sprintf("%s.json", prefix)
wrappedData := FulfillmentResource{Fulfillment: &fulfillment}
resource := new(FulfillmentResource)
52 changes: 52 additions & 0 deletions fulfillment_order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package goshopify

import (
"fmt"
)

// FulfillmentOrderService is an interface for interfacing with the fulfillment endpoints
// of the Shopify API.
// https://shopify.dev/api/admin-rest/2022-10/resources/fulfillmentorder
type FulfillmentOrderService interface {
List(interface{}) ([]FulfillmentOrder, error)
}

// FulfillmentOrdersService is an interface for other Shopify resources
// to interface with the fulfillment endpoints of the Shopify API.
// https://help.shopify.com/api/reference/fulfillment
type FulfillmentOrdersService interface {
ListFulfillmentOrders(int64, interface{}) ([]FulfillmentOrder, error)
}

// FulfillmentOrderServiceOp handles communication with the fulfillment order
// related methods of the Shopify API.
type FulfillmentOrderServiceOp struct {
client *Client
resource string
resourceID int64
}

// FulfillmentOrder represents a Shopify fulfillment order.
type FulfillmentOrder struct {
ID int64 `json:"id,omitempty"`
ShopID int64 `json:"shop_id,omitempty"`
OrderID int64 `json:"order_id,omitempty"`
Status string `json:"status,omitempty"`
LineItems []FulfillmentOrderLineItem `json:"line_items"`
// Note: There are a lot of other possible fields however current we only care about ID and LineItems
// https://shopify.dev/api/admin-rest/2022-10/resources/fulfillmentorder#resource-object
}

// FulfillmentOrdersResource represents the result from the orders/{order_id}/fulfillment_orders.json endpoint
type FulfillmentOrdersResource struct {
FulfillmentOrders []FulfillmentOrder `json:"fulfillment_orders"`
}

// List fulfillment orders
func (s *FulfillmentOrderServiceOp) List(options interface{}) ([]FulfillmentOrder, error) {
prefix := FulfillmentOrderPathPrefix(s.resource, s.resourceID)
path := fmt.Sprintf("%s.json", prefix)
resource := new(FulfillmentOrdersResource)
err := s.client.Get(path, resource, options)
return resource.FulfillmentOrders, err
}
29 changes: 29 additions & 0 deletions fulfillment_order_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package goshopify

import (
"fmt"
"reflect"
"testing"

"github.com/jarcoal/httpmock"
)

func TestFulfillmentOrderList(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/orders/123/fulfillment_orders.json", client.pathPrefix),
httpmock.NewStringResponder(200, `{"fulfillment_orders": [{"id":1},{"id":2}]}`))

fulfillmentOrderService := &FulfillmentOrderServiceOp{client: client, resource: ordersResourceName, resourceID: 123}

fulfillmentOrders, err := fulfillmentOrderService.List(nil)
if err != nil {
t.Errorf("FulfillmentOrder.List returned error: %v", err)
}

expected := []FulfillmentOrder{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(fulfillmentOrders, expected) {
t.Errorf("FulfillmentOrder.List returned %+v, expected %+v", fulfillmentOrders, expected)
}
}
15 changes: 14 additions & 1 deletion fulfillment_test.go
Original file line number Diff line number Diff line change
@@ -99,19 +99,32 @@ func TestFulfillmentCreate(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder("POST", fmt.Sprintf("https://fooshop.myshopify.com/%s/orders/123/fulfillments.json", client.pathPrefix),
httpmock.RegisterResponder("POST", fmt.Sprintf("https://fooshop.myshopify.com/%s/fulfillments.json", client.pathPrefix),
httpmock.NewBytesResponder(200, loadFixture("fulfillment.json")))

fulfillmentService := &FulfillmentServiceOp{client: client, resource: ordersResourceName, resourceID: 123}

fulfillment := Fulfillment{
OrderID: 123,
LocationID: 905684977,
TrackingNumber: "123456789",
TrackingUrls: []string{
"https://shipping.xyz/track.php?num=123456789",
"https://anothershipper.corp/track.php?code=abc",
},
NotifyCustomer: true,
LineItemsByFulfillmentOrder: []LineItemByFulfillmentOrder{
{
FulfillmentOrderID: 333,
FulfillmentOrderLineItems: []FulfillmentOrderLineItem{
{
ID: 987,
ShopID: 1,
FulfillmentOrderID: 123,
},
},
},
},
}

returnedFulfillment, err := fulfillmentService.Create(fulfillment)
34 changes: 24 additions & 10 deletions goshopify.go
Original file line number Diff line number Diff line change
@@ -121,9 +121,12 @@ type Client struct {
// A general response error that follows a similar layout to Shopify's response
// errors, i.e. either a single message or a list of messages.
type ResponseError struct {
Status int
Message string
Errors []string
Status int
Message string
Errors []string
Body string
Request *http.Request
Response *http.Response
}

// GetStatus returns http response status
@@ -143,17 +146,25 @@ func (e ResponseError) GetErrors() []string {

func (e ResponseError) Error() string {
if e.Message != "" {
return e.Message
return fmt.Sprintf("ERROR: %v : %v", e.Message, e.Body)
}

sort.Strings(e.Errors)
s := strings.Join(e.Errors, ", ")

if s != "" {
return s
return fmt.Sprintf("ERRORS: %v : %v", s, e.Body)
}

return "Unknown Error"
return fmt.Sprintf("UNKNOWN ERROR: Body: %v, Request: %#v, Response: %#v", e.Body, e.Request, e.Response)
}

func (e ResponseError) GetResponse() *http.Response {
return e.Response
}

func (e ResponseError) GetRequest() *http.Request {
return e.Request
}

// ResponseDecodingError occurs when the response body from Shopify could
@@ -328,7 +339,7 @@ func (c *Client) doGetHeaders(req *http.Request, v interface{}) (http.Header, er
return nil, err // http client errors, not api responses
}

respErr := CheckResponseError(resp)
respErr := CheckResponseError(resp, req)
if respErr == nil {
break // no errors, break out of the retry loop
}
@@ -445,7 +456,7 @@ func wrapSpecificError(r *http.Response, err ResponseError) error {
return err
}

func CheckResponseError(r *http.Response) error {
func CheckResponseError(r *http.Response, req *http.Request) error {
if http.StatusOK <= r.StatusCode && r.StatusCode < http.StatusMultipleChoices {
return nil
}
@@ -476,8 +487,11 @@ func CheckResponseError(r *http.Response) error {

// Create the response error from the Shopify error.
responseError := ResponseError{
Status: r.StatusCode,
Message: shopifyError.Error,
Status: r.StatusCode,
Message: shopifyError.Error,
Body: string(bodyBytes),
Response: r,
Request: req,
}

// If the errors field is not filled out, we can return here.
34 changes: 21 additions & 13 deletions order.go
Original file line number Diff line number Diff line change
@@ -31,6 +31,9 @@ type OrderService interface {

// FulfillmentsService used for Order resource to communicate with Fulfillments resource
FulfillmentsService

// FulfillmentOrdersService used for Order resource to communicate with FulfillmentOrders resource
FulfillmentOrdersService
}

// OrderServiceOp handles communication with the order related methods of the
@@ -194,8 +197,6 @@ type LineItem struct {
Grams int `json:"grams,omitempty"`
FulfillmentStatus string `json:"fulfillment_status,omitempty"`
TaxLines []TaxLine `json:"tax_lines,omitempty"`
OriginLocation *Address `json:"origin_location,omitempty"`
DestinationLocation *Address `json:"destination_location,omitempty"`
AppliedDiscount *AppliedDiscount `json:"applied_discount,omitempty"`
DiscountAllocations []DiscountAllocations `json:"discount_allocations,omitempty"`
}
@@ -239,7 +240,7 @@ func (li *LineItem) UnmarshalJSON(data []byte) error {
return err
}
li.Properties = p
} else { // else we unmarshal it into a struct
} else if aux.Properties != nil { // else we unmarshal it into a struct if set
var p NoteAttribute
err = json.Unmarshal(aux.Properties, &p)
if err != nil {
@@ -283,16 +284,17 @@ type PaymentDetails struct {
}

type ShippingLines struct {
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Price *decimal.Decimal `json:"price,omitempty"`
Code string `json:"code,omitempty"`
Source string `json:"source,omitempty"`
Phone string `json:"phone,omitempty"`
RequestedFulfillmentServiceID string `json:"requested_fulfillment_service_id,omitempty"`
DeliveryCategory string `json:"delivery_category,omitempty"`
CarrierIdentifier string `json:"carrier_identifier,omitempty"`
TaxLines []TaxLine `json:"tax_lines,omitempty"`
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Price *decimal.Decimal `json:"price,omitempty"`
Code string `json:"code,omitempty"`
Source string `json:"source,omitempty"`
Phone string `json:"phone,omitempty"`
RequestedFulfillmentServiceID string `json:"requested_fulfillment_service_id,omitempty"`
DeliveryCategory string `json:"delivery_category,omitempty"`
CarrierIdentifier string `json:"carrier_identifier,omitempty"`
TaxLines []TaxLine `json:"tax_lines,omitempty"`
DiscountAllocations []DiscountAllocations `json:"discount_allocations,omitempty"`
}

// UnmarshalJSON custom unmarshaller for ShippingLines implemented to handle requested_fulfillment_service_id being
@@ -545,3 +547,9 @@ func (s *OrderServiceOp) CancelFulfillment(orderID int64, fulfillmentID int64) (
fulfillmentService := &FulfillmentServiceOp{client: s.client, resource: ordersResourceName, resourceID: orderID}
return fulfillmentService.Cancel(fulfillmentID)
}

// List fulfillments for an order
func (s *OrderServiceOp) ListFulfillmentOrders(orderID int64, options interface{}) ([]FulfillmentOrder, error) {
fulfillmentOrderService := &FulfillmentOrderServiceOp{client: s.client, resource: ordersResourceName, resourceID: orderID}
return fulfillmentOrderService.List(options)
}
89 changes: 32 additions & 57 deletions order_test.go
Original file line number Diff line number Diff line change
@@ -674,17 +674,30 @@ func TestOrderCreateFulfillment(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder("POST", fmt.Sprintf("https://fooshop.myshopify.com/%s/orders/1/fulfillments.json", client.pathPrefix),
httpmock.RegisterResponder("POST", fmt.Sprintf("https://fooshop.myshopify.com/%s/fulfillments.json", client.pathPrefix),
httpmock.NewBytesResponder(200, loadFixture("fulfillment.json")))

fulfillment := Fulfillment{
OrderID: 1,
LocationID: 905684977,
TrackingNumber: "123456789",
TrackingUrls: []string{
"https://shipping.xyz/track.php?num=123456789",
"https://anothershipper.corp/track.php?code=abc",
},
NotifyCustomer: true,
LineItemsByFulfillmentOrder: []LineItemByFulfillmentOrder{
{
FulfillmentOrderID: 333,
FulfillmentOrderLineItems: []FulfillmentOrderLineItem{
{
ID: 987,
ShopID: 1,
FulfillmentOrderID: 123,
},
},
},
},
}

returnedFulfillment, err := client.Order.CreateFulfillment(1, fulfillment)
@@ -1041,26 +1054,6 @@ func testLineItem(t *testing.T, expected, actual LineItem) {

testTaxLines(t, expected.TaxLines, actual.TaxLines)

if actual.OriginLocation == nil {
if actual.OriginLocation != expected.OriginLocation {
t.Errorf("LineItem.OriginLocation should be (%v), was (%v)", expected.OriginLocation, actual.OriginLocation)
}
} else {
if *actual.OriginLocation != *expected.OriginLocation {
t.Errorf("LineItem.OriginLocation should be (%v), was (%v)", expected.OriginLocation, actual.OriginLocation)
}
}

if actual.DestinationLocation == nil {
if actual.DestinationLocation != expected.DestinationLocation {
t.Errorf("LineItem.DestinationLocation should be (%v), was (%v)", expected.DestinationLocation, actual.DestinationLocation)
}
} else {
if *actual.DestinationLocation != *expected.DestinationLocation {
t.Errorf("LineItem.DestinationLocation should be (%v), was (%v)", expected.DestinationLocation, actual.DestinationLocation)
}
}

if actual.AppliedDiscount == nil {
if actual.AppliedDiscount != expected.AppliedDiscount {
t.Errorf("LineItem.AppliedDiscount should be (%v), was (%v)", expected.AppliedDiscount, actual.AppliedDiscount)
@@ -1218,42 +1211,6 @@ func validLineItem() LineItem {
Rate: &tl2Rate,
},
},
OriginLocation: &Address{
ID: 123,
Address1: "100 some street",
Address2: "",
City: "Winnipeg",
Company: "Acme Corporation",
Country: "Canada",
CountryCode: "CA",
FirstName: "Bob",
LastName: "Smith",
Latitude: 49.811550,
Longitude: -97.189480,
Name: "test address",
Phone: "8675309",
Province: "Manitoba",
ProvinceCode: "MB",
Zip: "R3Y 0L6",
},
DestinationLocation: &Address{
ID: 124,
Address1: "200 some street",
Address2: "",
City: "Winnipeg",
Company: "Acme Corporation",
Country: "Canada",
CountryCode: "CA",
FirstName: "Bob",
LastName: "Smith",
Latitude: 49.811550,
Longitude: -97.189480,
Name: "test address",
Phone: "8675309",
Province: "Manitoba",
ProvinceCode: "MB",
Zip: "R3Y 0L6",
},
AppliedDiscount: &AppliedDiscount{
Title: "test discount",
Description: "my test discount",
@@ -1310,3 +1267,21 @@ func validShippingLines() ShippingLines {
},
}
}

func TestOrderListFulfillmentOrders(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/orders/1/fulfillment_orders.json", client.pathPrefix),
httpmock.NewStringResponder(200, `{"fulfillment_orders": [{"id":1},{"id":2}]}`))

fulfillmentOrders, err := client.Order.ListFulfillmentOrders(1, nil)
if err != nil {
t.Errorf("Order.ListFulfillmentOrders() returned error: %v", err)
}

expected := []FulfillmentOrder{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(fulfillmentOrders, expected) {
t.Errorf("Order.ListFulfillmentOrders() returned %+v, expected %+v", fulfillmentOrders, expected)
}
}
24 changes: 15 additions & 9 deletions util.go
Original file line number Diff line number Diff line change
@@ -29,20 +29,26 @@ func ShopBaseUrl(name string) string {
return fmt.Sprintf("https://%s", name)
}

// Return the prefix for a metafield path
func MetafieldPathPrefix(resource string, resourceID int64) string {
prefix := "metafields"
// Return the prefix for an order path
func OrderPathPrefix(prefix string, resource string, resourceID int64) string {
if resource != "" {
prefix = fmt.Sprintf("%s/%d/metafields", resource, resourceID)
return fmt.Sprintf("%s/%d/%s", resource, resourceID, prefix)
}

return prefix
}

// Return the prefix for a metafield path
func MetafieldPathPrefix(resource string, resourceID int64) string {
return OrderPathPrefix("metafields", resource, resourceID)
}

// Return the prefix for a fulfillment path
func FulfillmentPathPrefix(resource string, resourceID int64) string {
prefix := "fulfillments"
if resource != "" {
prefix = fmt.Sprintf("%s/%d/fulfillments", resource, resourceID)
}
return prefix
return OrderPathPrefix("fulfillments", resource, resourceID)
}

// Return the prefix for a fulfillment order path
func FulfillmentOrderPathPrefix(resource string, resourceID int64) string {
return OrderPathPrefix("fulfillment_orders", resource, resourceID)
}
18 changes: 18 additions & 0 deletions util_test.go
Original file line number Diff line number Diff line change
@@ -100,3 +100,21 @@ func TestFulfillmentPathPrefix(t *testing.T) {
}
}
}

func TestFulfillmentOrderPathPrefix(t *testing.T) {
cases := []struct {
resource string
resourceID int64
expected string
}{
{"", 0, "fulfillment_orders"},
{"orders", 123, "orders/123/fulfillment_orders"},
}

for _, c := range cases {
actual := FulfillmentOrderPathPrefix(c.resource, c.resourceID)
if actual != c.expected {
t.Errorf("FulfillmentOrderPathPrefix(%s, %d): expected %s, actual %s", c.resource, c.resourceID, c.expected, actual)
}
}
}