Skip to content

Commit

Permalink
Add Fulfillment Service (bold-commerce#115)
Browse files Browse the repository at this point in the history
* Add Fulfillment Model Support (bold-commerce#17)
  • Loading branch information
gir authored and andrewhoff committed Sep 7, 2018
1 parent 80079f3 commit 175a42d
Show file tree
Hide file tree
Showing 7 changed files with 580 additions and 23 deletions.
1 change: 1 addition & 0 deletions fixtures/fulfillment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"fulfillment":{"id":1022782888,"order_id":450789469,"status":"success","created_at":"2018-07-05T13:08:39-04:00","service":"manual","updated_at":"2018-07-05T13:08:40-04:00","tracking_company":"Bluedart","shipment_status":null,"location_id":905684977,"tracking_number":"123456789","tracking_numbers":["123456789"],"tracking_url":"https://shipping.xyz/track.php?num=123456789","tracking_urls":["https://shipping.xyz/track.php?num=123456789","https://anothershipper.corp/track.php?code=abc"],"receipt":{},"name":"#1001.1","admin_graphql_api_id":"gid://shopify/Fulfillment/1022782888","line_items":[{"id":466157049,"variant_id":39072856,"title":"IPod Nano - 8gb","quantity":1,"price":"199.00","sku":"IPOD2008GREEN","variant_title":"green","vendor":null,"fulfillment_service":"manual","product_id":632910392,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"IPod Nano - 8gb - green","variant_inventory_management":"shopify","properties":[{"name":"Custom Engraving Front","value":"Happy Birthday"},{"name":"Custom Engraving Back","value":"Merry Christmas"}],"product_exists":true,"fulfillable_quantity":0,"grams":200,"total_discount":"0.00","fulfillment_status":"fulfilled","discount_allocations":[],"admin_graphql_api_id":"gid://shopify/LineItem/466157049","tax_lines":[{"title":"State Tax","price":"3.98","rate":0.06}]},{"id":518995019,"variant_id":49148385,"title":"IPod Nano - 8gb","quantity":1,"price":"199.00","sku":"IPOD2008RED","variant_title":"red","vendor":null,"fulfillment_service":"manual","product_id":632910392,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"IPod Nano - 8gb - red","variant_inventory_management":"shopify","properties":[],"product_exists":true,"fulfillable_quantity":0,"grams":200,"total_discount":"0.00","fulfillment_status":"fulfilled","discount_allocations":[],"admin_graphql_api_id":"gid://shopify/LineItem/518995019","tax_lines":[{"title":"State Tax","price":"3.98","rate":0.06}]},{"id":703073504,"variant_id":457924702,"title":"IPod Nano - 8gb","quantity":1,"price":"199.00","sku":"IPOD2008BLACK","variant_title":"black","vendor":null,"fulfillment_service":"manual","product_id":632910392,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"IPod Nano - 8gb - black","variant_inventory_management":"shopify","properties":[],"product_exists":true,"fulfillable_quantity":0,"grams":200,"total_discount":"0.00","fulfillment_status":"fulfilled","discount_allocations":[],"admin_graphql_api_id":"gid://shopify/LineItem/703073504","tax_lines":[{"title":"State Tax","price":"3.98","rate":0.06}]}]}}
150 changes: 150 additions & 0 deletions fulfillment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package goshopify

import (
"fmt"
"time"
)

// FulfillmentService is an interface for interfacing with the fulfillment endpoints
// of the Shopify API.
// https://help.shopify.com/api/reference/fulfillment
type FulfillmentService interface {
List(interface{}) ([]Fulfillment, error)
Count(interface{}) (int, error)
Get(int, interface{}) (*Fulfillment, error)
Create(Fulfillment) (*Fulfillment, error)
Update(Fulfillment) (*Fulfillment, error)
Complete(int) (*Fulfillment, error)
Transition(int) (*Fulfillment, error)
Cancel(int) (*Fulfillment, error)
}

// FulfillmentsService 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 FulfillmentsService interface {
ListFulfillments(int, interface{}) ([]Fulfillment, error)
CountFulfillments(int, interface{}) (int, error)
GetFulfillment(int, int, interface{}) (*Fulfillment, error)
CreateFulfillment(int, Fulfillment) (*Fulfillment, error)
UpdateFulfillment(int, Fulfillment) (*Fulfillment, error)
CompleteFulfillment(int, int) (*Fulfillment, error)
TransitionFulfillment(int, int) (*Fulfillment, error)
CancelFulfillment(int, int) (*Fulfillment, error)
}

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

// Fulfillment represents a Shopify fulfillment.
type Fulfillment struct {
ID int `json:"id,omitempty"`
OrderID int `json:"order_id,omitempty"`
LocationID int `json:"location_id,omitempty"`
Status string `json:"status,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
Service string `json:"service,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
TrackingCompany string `json:"tracking_company,omitempty"`
ShipmentStatus string `json:"shipment_status,omitempty"`
TrackingNumber string `json:"tracking_number,omitempty"`
TrackingNumbers []string `json:"tracking_numbers,omitempty"`
TrackingUrl string `json:"tracking_url,omitempty"`
TrackingUrls []string `json:"tracking_urls,omitempty"`
Receipt Receipt `json:"receipt,omitempty"`
LineItems []LineItem `json:"line_items,omitempty"`
NotifyCustomer bool `json:"notify_customer,omitempty"`
}

// Receipt represents a Shopify receipt.
type Receipt struct {
TestCase bool `json:"testcase,omitempty"`
Authorization string `json:"authorization,omitempty"`
}

// FulfillmentResource represents the result from the fulfillments/X.json endpoint
type FulfillmentResource struct {
Fulfillment *Fulfillment `json:"fulfillment"`
}

// FulfillmentsResource represents the result from the fullfilments.json endpoint
type FulfillmentsResource struct {
Fulfillments []Fulfillment `json:"fulfillments"`
}

// List fulfillments
func (s *FulfillmentServiceOp) List(options interface{}) ([]Fulfillment, error) {
prefix := FulfillmentPathPrefix(s.resource, s.resourceID)
path := fmt.Sprintf("%s.json", prefix)
resource := new(FulfillmentsResource)
err := s.client.Get(path, resource, options)
return resource.Fulfillments, err
}

// Count fulfillments
func (s *FulfillmentServiceOp) Count(options interface{}) (int, error) {
prefix := FulfillmentPathPrefix(s.resource, s.resourceID)
path := fmt.Sprintf("%s/count.json", prefix)
return s.client.Count(path, options)
}

// Get individual fulfillment
func (s *FulfillmentServiceOp) Get(fulfillmentID int, options interface{}) (*Fulfillment, error) {
prefix := FulfillmentPathPrefix(s.resource, s.resourceID)
path := fmt.Sprintf("%s/%d.json", prefix, fulfillmentID)
resource := new(FulfillmentResource)
err := s.client.Get(path, resource, options)
return resource.Fulfillment, err
}

// Create a new fulfillment
func (s *FulfillmentServiceOp) Create(fulfillment Fulfillment) (*Fulfillment, error) {
prefix := FulfillmentPathPrefix(s.resource, s.resourceID)
path := fmt.Sprintf("%s.json", prefix)
wrappedData := FulfillmentResource{Fulfillment: &fulfillment}
resource := new(FulfillmentResource)
err := s.client.Post(path, wrappedData, resource)
return resource.Fulfillment, err
}

// Update an existing fulfillment
func (s *FulfillmentServiceOp) Update(fulfillment Fulfillment) (*Fulfillment, error) {
prefix := FulfillmentPathPrefix(s.resource, s.resourceID)
path := fmt.Sprintf("%s/%d.json", prefix, fulfillment.ID)
wrappedData := FulfillmentResource{Fulfillment: &fulfillment}
resource := new(FulfillmentResource)
err := s.client.Put(path, wrappedData, resource)
return resource.Fulfillment, err
}

// Complete an existing fulfillment
func (s *FulfillmentServiceOp) Complete(fulfillmentID int) (*Fulfillment, error) {
prefix := FulfillmentPathPrefix(s.resource, s.resourceID)
path := fmt.Sprintf("%s/%d/complete.json", prefix, fulfillmentID)
resource := new(FulfillmentResource)
err := s.client.Post(path, nil, resource)
return resource.Fulfillment, err
}

// Transition an existing fulfillment
func (s *FulfillmentServiceOp) Transition(fulfillmentID int) (*Fulfillment, error) {
prefix := FulfillmentPathPrefix(s.resource, s.resourceID)
path := fmt.Sprintf("%s/%d/open.json", prefix, fulfillmentID)
resource := new(FulfillmentResource)
err := s.client.Post(path, nil, resource)
return resource.Fulfillment, err
}

// Cancel an existing fulfillment
func (s *FulfillmentServiceOp) Cancel(fulfillmentID int) (*Fulfillment, error) {
prefix := FulfillmentPathPrefix(s.resource, s.resourceID)
path := fmt.Sprintf("%s/%d/cancel.json", prefix, fulfillmentID)
resource := new(FulfillmentResource)
err := s.client.Post(path, nil, resource)
return resource.Fulfillment, err
}
191 changes: 191 additions & 0 deletions fulfillment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package goshopify

import (
"reflect"
"testing"
"time"

httpmock "gopkg.in/jarcoal/httpmock.v1"
)

func FulfillmentTests(t *testing.T, fulfillment Fulfillment) {
// Check that ID is assigned to the returned fulfillment
expectedInt := 1022782888
if fulfillment.ID != expectedInt {
t.Errorf("Fulfillment.ID returned %+v, expected %+v", fulfillment.ID, expectedInt)
}
}

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

httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/123/fulfillments.json",
httpmock.NewStringResponder(200, `{"fulfillments": [{"id":1},{"id":2}]}`))

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

fulfillments, err := fulfillmentService.List(nil)
if err != nil {
t.Errorf("Fulfillment.List returned error: %v", err)
}

expected := []Fulfillment{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(fulfillments, expected) {
t.Errorf("Fulfillment.List returned %+v, expected %+v", fulfillments, expected)
}
}

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

httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/count.json",
httpmock.NewStringResponder(200, `{"count": 3}`))

httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/count.json?created_at_min=2016-01-01T00%3A00%3A00Z",
httpmock.NewStringResponder(200, `{"count": 2}`))

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

cnt, err := fulfillmentService.Count(nil)
if err != nil {
t.Errorf("Fulfillment.Count returned error: %v", err)
}

expected := 3
if cnt != expected {
t.Errorf("Fulfillment.Count returned %d, expected %d", cnt, expected)
}

date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC)
cnt, err = fulfillmentService.Count(CountOptions{CreatedAtMin: date})
if err != nil {
t.Errorf("Fulfillment.Count returned error: %v", err)
}

expected = 2
if cnt != expected {
t.Errorf("Fulfillment.Count returned %d, expected %d", cnt, expected)
}
}

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

httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1.json",
httpmock.NewStringResponder(200, `{"fulfillment": {"id":1}}`))

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

fulfillment, err := fulfillmentService.Get(1, nil)
if err != nil {
t.Errorf("Fulfillment.Get returned error: %v", err)
}

expected := &Fulfillment{ID: 1}
if !reflect.DeepEqual(fulfillment, expected) {
t.Errorf("Fulfillment.Get returned %+v, expected %+v", fulfillment, expected)
}
}

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

httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/orders/123/fulfillments.json",
httpmock.NewBytesResponder(200, loadFixture("fulfillment.json")))

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

fulfillment := Fulfillment{
LocationID: 905684977,
TrackingNumber: "123456789",
TrackingUrls: []string{
"https://shipping.xyz/track.php?num=123456789",
"https://anothershipper.corp/track.php?code=abc",
},
NotifyCustomer: true,
}

returnedFulfillment, err := fulfillmentService.Create(fulfillment)
if err != nil {
t.Errorf("Fulfillment.Create returned error: %v", err)
}

FulfillmentTests(t, *returnedFulfillment)
}

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

httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1022782888.json",
httpmock.NewBytesResponder(200, loadFixture("fulfillment.json")))

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

fulfillment := Fulfillment{
ID: 1022782888,
TrackingNumber: "987654321",
}

returnedFulfillment, err := fulfillmentService.Update(fulfillment)
if err != nil {
t.Errorf("Fulfillment.Update returned error: %v", err)
}

FulfillmentTests(t, *returnedFulfillment)
}

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

httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1/complete.json",
httpmock.NewBytesResponder(200, loadFixture("fulfillment.json")))

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

returnedFulfillment, err := fulfillmentService.Complete(1)
if err != nil {
t.Errorf("Fulfillment.Complete returned error: %v", err)
}

FulfillmentTests(t, *returnedFulfillment)
}

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

httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1/open.json",
httpmock.NewBytesResponder(200, loadFixture("fulfillment.json")))

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

returnedFulfillment, err := fulfillmentService.Transition(1)
if err != nil {
t.Errorf("Fulfillment.Transition returned error: %v", err)
}

FulfillmentTests(t, *returnedFulfillment)
}

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

httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1/cancel.json",
httpmock.NewBytesResponder(200, loadFixture("fulfillment.json")))

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

returnedFulfillment, err := fulfillmentService.Cancel(1)
if err != nil {
t.Errorf("Fulfillment.Cancel returned error: %v", err)
}

FulfillmentTests(t, *returnedFulfillment)
}
Loading

0 comments on commit 175a42d

Please sign in to comment.