├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── applicationcharge.go ├── applicationcharge_test.go ├── asset.go ├── asset_test.go ├── blog.go ├── blog_test.go ├── customcollection.go ├── customcollection_test.go ├── customer.go ├── customer_address.go ├── customer_test.go ├── docker-compose.yml ├── fixtures ├── applicationcharge.json ├── asset.json ├── blog.json ├── customcollection.json ├── customer.json ├── fulfillment.json ├── image.json ├── images.json ├── metafield.json ├── order.json ├── order_with_transaction.json ├── orders.json ├── page.json ├── product.json ├── reccuringapplicationcharge │ ├── reccuringapplicationcharge.json │ ├── reccuringapplicationcharge_all_fields_affected.json │ ├── reccuringapplicationcharge_bad.json │ ├── reccuringapplicationcharge_bad_activated_on.json │ ├── reccuringapplicationcharge_bad_billing_on.json │ ├── reccuringapplicationcharge_bad_cancelled_on.json │ ├── reccuringapplicationcharge_bad_created_at.json │ ├── reccuringapplicationcharge_bad_trial_ends_on.json │ └── reccuringapplicationcharge_bad_updated_at.json ├── redirect.json ├── script_tags.json ├── shop.json ├── smartcollection.json ├── transaction.json ├── transactions.json ├── variant.json ├── webhook.json └── webhooks.json ├── fulfillment.go ├── fulfillment_test.go ├── goshopify.go ├── goshopify_test.go ├── image.go ├── image_test.go ├── metafield.go ├── metafield_test.go ├── oauth.go ├── oauth_test.go ├── order.go ├── order_test.go ├── page.go ├── page_test.go ├── product.go ├── product_test.go ├── recurringapplicationcharge.go ├── recurringapplicationcharge_test.go ├── redirect.go ├── redirect_test.go ├── scripttag.go ├── scripttag_test.go ├── shop.go ├── shop_test.go ├── smartcollection.go ├── smartcollection_test.go ├── theme.go ├── theme_test.go ├── transaction.go ├── transaction_test.go ├── util.go ├── util_test.go ├── variant.go ├── variant_test.go ├── webhook.go └── webhook_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | coverage.* 3 | .vscode 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.7" 4 | - "1.8" 5 | - "1.9" 6 | - "1.10" 7 | script: 8 | - go test -coverprofile=coverage.txt 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) 11 | notifications: 12 | slack: 13 | secure: H5CP+m2bq7m2IyKafiwsgpINE/qKWknbGw+r5hVs6VA8kzAsUs9rNa6d6wcDcqQoadU17vytrOyzAxasAYBkjBFUijDLqp6o0LFhC37fHH7jOIL7eh2X1CkASjYmP8JO7LySgNmLVmFFxZihKcpCFjrbwfU8UT4qkpuAbdIubyZ8Nu2QlYwev47MIpP/C2Jc9QzmTmAz4Bqtmgm45bEL33E4iAZNMgULvZDs4A4zZFxQma6E0+YMeiXNiHfR0e/r1emztUbE15YcHqiW+TqQomFowANrSu3UJQPDbi2qTe3vNyK1EK+HG8O55wL3AgtilPepvwLkpzoe05GYSUN9K0gECVA1AUJQZQJi0XwqAE3S0Iimc3aqu2wUCrHCwz5dkiDPmiitSDyGWpwETznV2579RmC4bJCF+ovLS21MHzlooLhOXqJSxz5aA3Ikm5PsJpbL3xWfNUEuU8C34G9Sq49XAymIDijUIrEyDN1q3Azs13ZvYATt88LKUnGcKa+XMIVnfTQ7M630WXtmQb8yOngn3tBqHuS1si6PiZYlylCElYa1AaISJKZliwV17QvsaNcvxROb9oNfrEg7LBZmpxU2ntKdmznEgmtUP9f2OcOeyJKuzFYC6dvgx8PzyUuuGSbnuEBQKAP43Ij2i7J7dBgdIcWrsN/cm55x2QIZ5eY= 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.9 2 | 3 | # This is similar to the golang-onbuild image but with different paths and 4 | # test-dependencies loaded as well. 5 | RUN mkdir -p /go/src/github.com/getconversio/go-shopify 6 | WORKDIR /go/src/github.com/getconversio/go-shopify 7 | 8 | COPY . /go/src/github.com/getconversio/go-shopify 9 | RUN go get -v -d -t 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Conversio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATION NOTICE 2 | Continuing support for the go-shopify library will be at Bold Commerce's fork over [here](https://github.com/bold-commerce/go-shopify). Please open issues and pull requests over there. 3 | # go-shopify 4 | 5 | Another Shopify Api Library in Go. 6 | 7 | **Note**: The library does not have implementations of all Shopify resources, but it is being used in production by Conversio and should be stable for usage. PRs for new resources and endpoints are welcome, or you can simply implement some yourself as-you-go. See the section "Using your own models" for more info. 8 | 9 | [![Build Status](https://travis-ci.org/getconversio/go-shopify.svg?branch=master)](https://travis-ci.org/getconversio/go-shopify) 10 | [![codecov](https://codecov.io/gh/getconversio/go-shopify/branch/master/graph/badge.svg)](https://codecov.io/gh/getconversio/go-shopify) 11 | 12 | ## Install 13 | 14 | ```console 15 | $ go get github.com/getconversio/go-shopify 16 | ``` 17 | 18 | ## Use 19 | 20 | ```go 21 | import "github.com/getconversio/go-shopify" 22 | ``` 23 | 24 | This gives you access to the `goshopify` package. 25 | 26 | #### Oauth 27 | 28 | If you don't have an access token yet, you can obtain one with the oauth flow. 29 | Something like this will work: 30 | 31 | ```go 32 | // Create an app somewhere. 33 | app := goshopify.App{ 34 | ApiKey: "abcd", 35 | ApiSecret: "efgh", 36 | RedirectUrl: "https://example.com/shopify/callback", 37 | Scope: "read_products,read_orders", 38 | } 39 | 40 | // Create an oauth-authorize url for the app and redirect to it. 41 | // In some request handler, you probably want something like this: 42 | func MyHandler(w http.ResponseWriter, r *http.Request) { 43 | shopName := r.URL.Query().Get("shop") 44 | authUrl := app.AuthorizeURL(shopName) 45 | http.Redirect(w, r, authUrl, http.StatusFound) 46 | } 47 | 48 | // Fetch a permanent access token in the callback 49 | func MyCallbackHandler(w http.ResponseWriter, r *http.Request) { 50 | // Check that the callback signature is valid 51 | if ok, _ := app.VerifyAuthorizationURL(r.URL); !ok { 52 | http.Error(w, "Invalid Signature", http.StatusUnauthorized) 53 | return 54 | } 55 | 56 | query := r.URL.Query() 57 | shopName := query.Get("shop") 58 | code := query.Get("code") 59 | token, err := app.GetAccessToken(shopName, code) 60 | 61 | // Do something with the token, like store it in a DB. 62 | } 63 | ``` 64 | 65 | #### Api calls with a token 66 | 67 | With a permanent access token, you can make API calls like this: 68 | 69 | ```go 70 | // Create an app somewhere. 71 | app := goshopify.App{ 72 | ApiKey: "abcd", 73 | ApiSecret: "efgh", 74 | RedirectUrl: "https://example.com/shopify/callback", 75 | Scope: "read_products", 76 | } 77 | 78 | // Create a new API client 79 | client := goshopify.NewClient(app, "shopname", "token") 80 | 81 | // Fetch the number of products. 82 | numProducts, err := client.Product.Count(nil) 83 | ``` 84 | 85 | #### Private App Auth 86 | 87 | Private Shopify apps use basic authentication and do not require going through the OAuth flow. Here is an example: 88 | 89 | ```go 90 | // Create an app somewhere. 91 | app := goshopify.App{ 92 | ApiKey: "apikey", 93 | Password: "apipassword", 94 | } 95 | 96 | // Create a new API client (notice the token parameter is the empty string) 97 | client := goshopify.NewClient(app, "shopname", "") 98 | 99 | // Fetch the number of products. 100 | numProducts, err := client.Product.Count(nil) 101 | ``` 102 | 103 | #### Query options 104 | 105 | Most API functions take an options `interface{}` as parameter. You can use one 106 | from the library or create your own. For example, to fetch the number of 107 | products created after January 1, 2016, you can do: 108 | 109 | ```go 110 | // Create standard CountOptions 111 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 112 | options := goshopify.CountOptions{createdAtMin: date} 113 | 114 | // Use the options when calling the API. 115 | numProducts, err := client.Product.Count(options) 116 | ``` 117 | 118 | The options are parsed with Google's 119 | [go-querystring](https://github.com/google/go-querystring) library so you can 120 | use custom options like this: 121 | 122 | ```go 123 | // Create custom options for the orders. 124 | // Notice the `url:"status"` tag 125 | options := struct { 126 | Status string `url:"status"` 127 | }{"any"} 128 | 129 | // Fetch the order count for orders with status="any" 130 | orderCount, err := client.Order.Count(options) 131 | ``` 132 | 133 | #### Using your own models 134 | 135 | Not all endpoints are implemented right now. In those case, feel free to 136 | implement them and make a PR, or you can create your own struct for the data 137 | and use `NewRequest` with the API client. This is how the existing endpoints 138 | are implemented. 139 | 140 | For example, let's say you want to fetch webhooks. There's a helper function 141 | `Get` specifically for fetching stuff so this will work: 142 | 143 | ```go 144 | // Declare a model for the webhook 145 | type Webhook struct { 146 | ID int `json:"id"` 147 | Address string `json:"address"` 148 | } 149 | 150 | // Declare a model for the resource root. 151 | type WebhooksResource struct { 152 | Webhooks []Webhook `json:"webhooks"` 153 | } 154 | 155 | func FetchWebhooks() ([]Webhook, error) { 156 | path := "admin/webhooks.json" 157 | resource := new(WebhooksResoure) 158 | client := goshopify.NewClient(app, "shopname", "token") 159 | 160 | // resource gets modified when calling Get 161 | err := client.Get(path, resource, nil) 162 | 163 | return resource.Webhooks, err 164 | } 165 | ``` 166 | 167 | #### Webhooks verification 168 | 169 | In order to be sure that a webhook is sent from ShopifyApi you could easily verify 170 | it with the `VerifyWebhookRequest` method. 171 | 172 | For example: 173 | ```go 174 | func ValidateWebhook(httpRequest *http.Request) (bool) { 175 | shopifyApp := goshopify.App{ApiSecret: "ratz"} 176 | return shopifyApp.VerifyWebhookRequest(httpRequest) 177 | } 178 | ``` 179 | 180 | ## Develop and test 181 | 182 | There's nothing special to note about the tests except that if you have Docker 183 | and Compose installed, you can test like this: 184 | 185 | $ docker-compose build dev 186 | $ docker-compose run --rm dev 187 | 188 | Testing the package is the default command for the dev container. To create a 189 | coverage profile: 190 | 191 | $ docker-compose run --rm dev bash -c 'go test -coverprofile=coverage.out ./... && go tool cover -html coverage.out -o coverage.html' 192 | -------------------------------------------------------------------------------- /applicationcharge.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | const applicationChargesBasePath = "admin/application_charges" 11 | 12 | // ApplicationChargeService is an interface for interacting with the 13 | // ApplicationCharge endpoints of the Shopify API. 14 | // See https://help.shopify.com/api/reference/billing/applicationcharge 15 | type ApplicationChargeService interface { 16 | Create(ApplicationCharge) (*ApplicationCharge, error) 17 | Get(int, interface{}) (*ApplicationCharge, error) 18 | List(interface{}) ([]ApplicationCharge, error) 19 | Activate(ApplicationCharge) (*ApplicationCharge, error) 20 | } 21 | 22 | type ApplicationChargeServiceOp struct { 23 | client *Client 24 | } 25 | 26 | type ApplicationCharge struct { 27 | ID int `json:"id"` 28 | Name string `json:"name"` 29 | APIClientID int `json:"api_client_id"` 30 | Price *decimal.Decimal `json:"price"` 31 | Status string `json:"status"` 32 | ReturnURL string `json:"return_url"` 33 | Test *bool `json:"test"` 34 | CreatedAt *time.Time `json:"created_at"` 35 | UpdatedAt *time.Time `json:"updated_at"` 36 | ChargeType *string `json:"charge_type"` 37 | DecoratedReturnURL string `json:"decorated_return_url"` 38 | ConfirmationURL string `json:"confirmation_url"` 39 | } 40 | 41 | // ApplicationChargeResource represents the result from the 42 | // admin/application_charges{/X{/activate.json}.json}.json endpoints. 43 | type ApplicationChargeResource struct { 44 | Charge *ApplicationCharge `json:"application_charge"` 45 | } 46 | 47 | // ApplicationChargesResource represents the result from the 48 | // admin/application_charges.json endpoint. 49 | type ApplicationChargesResource struct { 50 | Charges []ApplicationCharge `json:"application_charges"` 51 | } 52 | 53 | // Create creates new application charge. 54 | func (a ApplicationChargeServiceOp) Create(charge ApplicationCharge) (*ApplicationCharge, error) { 55 | path := fmt.Sprintf("%s.json", applicationChargesBasePath) 56 | resource := &ApplicationChargeResource{} 57 | return resource.Charge, a.client.Post(path, ApplicationChargeResource{Charge: &charge}, resource) 58 | } 59 | 60 | // Get gets individual application charge. 61 | func (a ApplicationChargeServiceOp) Get(chargeID int, options interface{}) (*ApplicationCharge, error) { 62 | path := fmt.Sprintf("%s/%d.json", applicationChargesBasePath, chargeID) 63 | resource := &ApplicationChargeResource{} 64 | return resource.Charge, a.client.Get(path, resource, options) 65 | } 66 | 67 | // List gets all application charges. 68 | func (a ApplicationChargeServiceOp) List(options interface{}) ([]ApplicationCharge, error) { 69 | path := fmt.Sprintf("%s.json", applicationChargesBasePath) 70 | resource := &ApplicationChargesResource{} 71 | return resource.Charges, a.client.Get(path, resource, options) 72 | } 73 | 74 | // Activate activates application charge. 75 | func (a ApplicationChargeServiceOp) Activate(charge ApplicationCharge) (*ApplicationCharge, error) { 76 | path := fmt.Sprintf("%s/%d/activate.json", applicationChargesBasePath, charge.ID) 77 | resource := &ApplicationChargeResource{} 78 | return resource.Charge, a.client.Post(path, ApplicationChargeResource{Charge: &charge}, resource) 79 | } 80 | -------------------------------------------------------------------------------- /applicationcharge_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "github.com/shopspring/decimal" 9 | "gopkg.in/jarcoal/httpmock.v1" 10 | ) 11 | 12 | // applicationChargeTests tests if the fields are properly parsed. 13 | func applicationChargeTests(t *testing.T, charge ApplicationCharge) { 14 | var nilTest *bool 15 | cases := []struct { 16 | field string 17 | expected interface{} 18 | actual interface{} 19 | }{ 20 | {"ID", 1017262355, charge.ID}, 21 | {"Name", "Super Duper Expensive action", charge.Name}, 22 | {"APIClientID", 755357713, charge.APIClientID}, 23 | {"Price", decimal.NewFromFloat(100.00).String(), charge.Price.String()}, 24 | {"Status", "pending", charge.Status}, 25 | {"ReturnURL", "http://super-duper.shopifyapps.com/", charge.ReturnURL}, 26 | {"Test", nilTest, charge.Test}, 27 | {"CreatedAt", "2018-07-05T13:11:28-04:00", charge.CreatedAt.Format(time.RFC3339)}, 28 | {"UpdatedAt", "2018-07-05T13:11:28-04:00", charge.UpdatedAt.Format(time.RFC3339)}, 29 | { 30 | "DecoratedReturnURL", 31 | "http://super-duper.shopifyapps.com/?charge_id=1017262355", 32 | charge.DecoratedReturnURL, 33 | }, 34 | { 35 | "ConfirmationURL", 36 | "https://apple.myshopify.com/admin/charges/1017262355/confirm_application_charge?sign" + 37 | "ature=BAhpBBMxojw%3D--1139a82a3433b1a6771786e03f02300440e11883", 38 | charge.ConfirmationURL, 39 | }, 40 | } 41 | 42 | for _, c := range cases { 43 | if c.expected != c.actual { 44 | t.Errorf("ApplicationCharge.%s returned %v, expected %v", c.field, c.actual, c.expected) 45 | } 46 | } 47 | } 48 | 49 | func TestApplicationChargeServiceOp_Create(t *testing.T) { 50 | setup() 51 | defer teardown() 52 | 53 | httpmock.RegisterResponder( 54 | "POST", 55 | "https://fooshop.myshopify.com/admin/application_charges.json", 56 | httpmock.NewBytesResponder(200, loadFixture("applicationcharge.json")), 57 | ) 58 | 59 | p := decimal.NewFromFloat(100.00) 60 | charge := ApplicationCharge{ 61 | Name: "Super Duper Expensive action", 62 | Price: &p, 63 | ReturnURL: "http://super-duper.shopifyapps.com", 64 | } 65 | 66 | returnedCharge, err := client.ApplicationCharge.Create(charge) 67 | if err != nil { 68 | t.Errorf("ApplicationCharge.Create returned an error: %v", err) 69 | } 70 | 71 | applicationChargeTests(t, *returnedCharge) 72 | } 73 | 74 | func TestApplicationChargeServiceOp_Get(t *testing.T) { 75 | setup() 76 | defer teardown() 77 | 78 | httpmock.RegisterResponder( 79 | "GET", 80 | "https://fooshop.myshopify.com/admin/application_charges/1.json", 81 | httpmock.NewStringResponder(200, `{"application_charge": {"id":1}}`), 82 | ) 83 | 84 | charge, err := client.ApplicationCharge.Get(1, nil) 85 | if err != nil { 86 | t.Errorf("ApplicationCharge.Get returned an error: %v", err) 87 | } 88 | 89 | expected := &ApplicationCharge{ID: 1} 90 | if !reflect.DeepEqual(charge, expected) { 91 | t.Errorf("ApplicationCharge.Get returned %+v, expected %+v", charge, expected) 92 | } 93 | } 94 | 95 | func TestApplicationChargeServiceOp_List(t *testing.T) { 96 | setup() 97 | defer teardown() 98 | 99 | httpmock.RegisterResponder( 100 | "GET", 101 | "https://fooshop.myshopify.com/admin/application_charges.json", 102 | httpmock.NewStringResponder(200, `{"application_charges": [{"id":1},{"id":2}]}`), 103 | ) 104 | 105 | charges, err := client.ApplicationCharge.List(nil) 106 | if err != nil { 107 | t.Errorf("ApplicationCharge.List returned an error: %v", err) 108 | } 109 | 110 | expected := []ApplicationCharge{{ID: 1}, {ID: 2}} 111 | if !reflect.DeepEqual(charges, expected) { 112 | t.Errorf("ApplicationCharge.List returned %+v, expected %+v", charges, expected) 113 | } 114 | } 115 | 116 | func TestApplicationChargeServiceOp_Activate(t *testing.T) { 117 | setup() 118 | defer teardown() 119 | 120 | httpmock.RegisterResponder( 121 | "POST", 122 | "https://fooshop.myshopify.com/admin/application_charges/455696195/activate.json", 123 | httpmock.NewStringResponder( 124 | 200, 125 | `{"application_charge":{"id":455696195,"status":"active"}}`, 126 | ), 127 | ) 128 | 129 | charge := ApplicationCharge{ 130 | ID: 455696195, 131 | Status: "accepted", 132 | } 133 | 134 | returnedCharge, err := client.ApplicationCharge.Activate(charge) 135 | if err != nil { 136 | t.Errorf("ApplicationCharge.Activate returned an error: %v", err) 137 | } 138 | 139 | expected := &ApplicationCharge{ID: 455696195, Status: "active"} 140 | if !reflect.DeepEqual(returnedCharge, expected) { 141 | t.Errorf("ApplicationCharge.Activate returned %+v, expected %+v", charge, expected) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /asset.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const assetsBasePath = "admin/themes" 9 | 10 | // AssetService is an interface for interfacing with the asset endpoints 11 | // of the Shopify API. 12 | // See: https://help.shopify.com/api/reference/asset 13 | type AssetService interface { 14 | List(int, interface{}) ([]Asset, error) 15 | Get(int, string) (*Asset, error) 16 | Update(int, Asset) (*Asset, error) 17 | Delete(int, string) error 18 | } 19 | 20 | // AssetServiceOp handles communication with the asset related methods of 21 | // the Shopify API. 22 | type AssetServiceOp struct { 23 | client *Client 24 | } 25 | 26 | // Asset represents a Shopify asset 27 | type Asset struct { 28 | Attachment string `json:"attachment"` 29 | ContentType string `json:"content_type"` 30 | Key string `json:"key"` 31 | PublicURL string `json:"public_url"` 32 | Size int `json:"size"` 33 | SourceKey string `json:"source_key"` 34 | Src string `json:"src"` 35 | ThemeID int `json:"theme_id"` 36 | Value string `json:"value"` 37 | CreatedAt *time.Time `json:"created_at"` 38 | UpdatedAt *time.Time `json:"updated_at"` 39 | } 40 | 41 | // AssetResource is the result from the themes/x/assets.json?asset[key]= endpoint 42 | type AssetResource struct { 43 | Asset *Asset `json:"asset"` 44 | } 45 | 46 | // AssetsResource is the result from the themes/x/assets.json endpoint 47 | type AssetsResource struct { 48 | Assets []Asset `json:"assets"` 49 | } 50 | 51 | type assetGetOptions struct { 52 | Key string `url:"asset[key]"` 53 | ThemeID int `url:"theme_id"` 54 | } 55 | 56 | // List the metadata for all assets in the given theme 57 | func (s *AssetServiceOp) List(themeID int, options interface{}) ([]Asset, error) { 58 | path := fmt.Sprintf("%s/%d/assets.json", assetsBasePath, themeID) 59 | resource := new(AssetsResource) 60 | err := s.client.Get(path, resource, options) 61 | return resource.Assets, err 62 | } 63 | 64 | // Get an asset by key from the given theme 65 | func (s *AssetServiceOp) Get(themeID int, key string) (*Asset, error) { 66 | path := fmt.Sprintf("%s/%d/assets.json", assetsBasePath, themeID) 67 | options := assetGetOptions{ 68 | Key: key, 69 | ThemeID: themeID, 70 | } 71 | resource := new(AssetResource) 72 | err := s.client.Get(path, resource, options) 73 | return resource.Asset, err 74 | } 75 | 76 | // Update an asset 77 | func (s *AssetServiceOp) Update(themeID int, asset Asset) (*Asset, error) { 78 | path := fmt.Sprintf("%s/%d/assets.json", assetsBasePath, themeID) 79 | wrappedData := AssetResource{Asset: &asset} 80 | resource := new(AssetResource) 81 | err := s.client.Put(path, wrappedData, resource) 82 | return resource.Asset, err 83 | } 84 | 85 | // Delete an asset 86 | func (s *AssetServiceOp) Delete(themeID int, key string) error { 87 | path := fmt.Sprintf("%s/%d/assets.json?asset[key]=%s", assetsBasePath, themeID, key) 88 | return s.client.Delete(path) 89 | } 90 | -------------------------------------------------------------------------------- /asset_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "gopkg.in/jarcoal/httpmock.v1" 8 | ) 9 | 10 | func assetTests(t *testing.T, asset Asset) { 11 | expectedKey := "templates/index.liquid" 12 | if asset.Key != expectedKey { 13 | t.Errorf("Asset.Key returned %+v, expected %+v", asset.Key, expectedKey) 14 | } 15 | } 16 | 17 | func TestAssetList(t *testing.T) { 18 | setup() 19 | defer teardown() 20 | 21 | httpmock.RegisterResponder( 22 | "GET", 23 | "https://fooshop.myshopify.com/admin/themes/1/assets.json", 24 | httpmock.NewStringResponder( 25 | 200, 26 | `{"assets": [{"key":"assets\/1.liquid"},{"key":"assets\/2.liquid"}]}`, 27 | ), 28 | ) 29 | 30 | assets, err := client.Asset.List(1, nil) 31 | if err != nil { 32 | t.Errorf("Asset.List returned error: %v", err) 33 | } 34 | 35 | expected := []Asset{{Key: "assets/1.liquid"}, {Key: "assets/2.liquid"}} 36 | if !reflect.DeepEqual(assets, expected) { 37 | t.Errorf("Asset.List returned %+v, expected %+v", assets, expected) 38 | } 39 | } 40 | 41 | func TestAssetGet(t *testing.T) { 42 | setup() 43 | defer teardown() 44 | 45 | httpmock.RegisterResponder( 46 | "GET", 47 | "https://fooshop.myshopify.com/admin/themes/1/assets.json?asset%5Bkey%5D=foo%2Fbar.liquid&theme_id=1", 48 | httpmock.NewStringResponder( 49 | 200, 50 | `{"asset": {"key":"foo\/bar.liquid"}}`, 51 | ), 52 | ) 53 | 54 | asset, err := client.Asset.Get(1, "foo/bar.liquid") 55 | if err != nil { 56 | t.Errorf("Asset.Get returned error: %v", err) 57 | } 58 | 59 | expected := &Asset{Key: "foo/bar.liquid"} 60 | if !reflect.DeepEqual(asset, expected) { 61 | t.Errorf("Asset.Get returned %+v, expected %+v", asset, expected) 62 | } 63 | } 64 | 65 | func TestAssetUpdate(t *testing.T) { 66 | setup() 67 | defer teardown() 68 | 69 | httpmock.RegisterResponder( 70 | "PUT", 71 | "https://fooshop.myshopify.com/admin/themes/1/assets.json", 72 | httpmock.NewBytesResponder( 73 | 200, 74 | loadFixture("asset.json"), 75 | ), 76 | ) 77 | 78 | asset := Asset{ 79 | Key: "templates/index.liquid", 80 | Value: "content", 81 | } 82 | 83 | returnedAsset, err := client.Asset.Update(1, asset) 84 | if err != nil { 85 | t.Errorf("Asset.Update returned error: %v", err) 86 | } 87 | if returnedAsset == nil { 88 | t.Errorf("Asset.Update returned nil") 89 | } 90 | } 91 | 92 | func TestAssetDelete(t *testing.T) { 93 | setup() 94 | defer teardown() 95 | 96 | httpmock.RegisterResponder( 97 | "DELETE", 98 | "https://fooshop.myshopify.com/admin/themes/1/assets.json?asset[key]=foo/bar.liquid", 99 | httpmock.NewStringResponder(200, "{}"), 100 | ) 101 | 102 | err := client.Asset.Delete(1, "foo/bar.liquid") 103 | if err != nil { 104 | t.Errorf("Asset.Delete returned error: %v", err) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /blog.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const blogsBasePath = "admin/blogs" 9 | 10 | // BlogService is an interface for interfacing with the blogs endpoints 11 | // of the Shopify API. 12 | // See: https://help.shopify.com/api/reference/online_store/blog 13 | type BlogService interface { 14 | List(interface{}) ([]Blog, error) 15 | Count(interface{}) (int, error) 16 | Get(int, interface{}) (*Blog, error) 17 | Create(Blog) (*Blog, error) 18 | Update(Blog) (*Blog, error) 19 | Delete(int) error 20 | } 21 | 22 | // BlogServiceOp handles communication with the blog related methods of 23 | // the Shopify API. 24 | type BlogServiceOp struct { 25 | client *Client 26 | } 27 | 28 | // Blog represents a Shopify blog 29 | type Blog struct { 30 | ID int `json:"id"` 31 | Title string `json:"title"` 32 | Commentable string `json:"commentable"` 33 | Feedburner string `json:"feedburner"` 34 | FeedburnerLocation string `json:"feedburner_location"` 35 | Handle string `json:"handle"` 36 | Metafield Metafield `json:"metafield"` 37 | Tags string `json:"tags"` 38 | TemplateSuffix string `json:"template_suffix"` 39 | CreatedAt *time.Time `json:"created_at"` 40 | UpdatedAt *time.Time `json:"updated_at"` 41 | } 42 | 43 | // BlogsResource is the result from the blogs.json endpoint 44 | type BlogsResource struct { 45 | Blogs []Blog `json:"blogs"` 46 | } 47 | 48 | // Represents the result from the blogs/X.json endpoint 49 | type BlogResource struct { 50 | Blog *Blog `json:"blog"` 51 | } 52 | 53 | // List all blogs 54 | func (s *BlogServiceOp) List(options interface{}) ([]Blog, error) { 55 | path := fmt.Sprintf("%s.json", blogsBasePath) 56 | resource := new(BlogsResource) 57 | err := s.client.Get(path, resource, options) 58 | return resource.Blogs, err 59 | } 60 | 61 | // Count blogs 62 | func (s *BlogServiceOp) Count(options interface{}) (int, error) { 63 | path := fmt.Sprintf("%s/count.json", blogsBasePath) 64 | return s.client.Count(path, options) 65 | } 66 | 67 | // Get single blog 68 | func (s *BlogServiceOp) Get(blogId int, options interface{}) (*Blog, error) { 69 | path := fmt.Sprintf("%s/%d.json", blogsBasePath, blogId) 70 | resource := new(BlogResource) 71 | err := s.client.Get(path, resource, options) 72 | return resource.Blog, err 73 | } 74 | 75 | // Create a new blog 76 | func (s *BlogServiceOp) Create(blog Blog) (*Blog, error) { 77 | path := fmt.Sprintf("%s.json", blogsBasePath) 78 | wrappedData := BlogResource{Blog: &blog} 79 | resource := new(BlogResource) 80 | err := s.client.Post(path, wrappedData, resource) 81 | return resource.Blog, err 82 | } 83 | 84 | // Update an existing blog 85 | func (s *BlogServiceOp) Update(blog Blog) (*Blog, error) { 86 | path := fmt.Sprintf("%s/%d.json", blogsBasePath, blog.ID) 87 | wrappedData := BlogResource{Blog: &blog} 88 | resource := new(BlogResource) 89 | err := s.client.Put(path, wrappedData, resource) 90 | return resource.Blog, err 91 | } 92 | 93 | // Delete an blog 94 | func (s *BlogServiceOp) Delete(blogId int) error { 95 | return s.client.Delete(fmt.Sprintf("%s/%d.json", blogsBasePath, blogId)) 96 | } 97 | -------------------------------------------------------------------------------- /blog_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "gopkg.in/jarcoal/httpmock.v1" 8 | ) 9 | 10 | func TestBlogList(t *testing.T) { 11 | setup() 12 | defer teardown() 13 | 14 | httpmock.RegisterResponder( 15 | "GET", 16 | "https://fooshop.myshopify.com/admin/blogs.json", 17 | httpmock.NewStringResponder( 18 | 200, 19 | `{"blogs": [{"id":1},{"id":2}]}`, 20 | ), 21 | ) 22 | 23 | blogs, err := client.Blog.List(nil) 24 | if err != nil { 25 | t.Errorf("Blog.List returned error: %v", err) 26 | } 27 | 28 | expected := []Blog{{ID: 1}, {ID: 2}} 29 | if !reflect.DeepEqual(blogs, expected) { 30 | t.Errorf("Blog.List returned %+v, expected %+v", blogs, expected) 31 | } 32 | 33 | } 34 | 35 | func TestBlogCount(t *testing.T) { 36 | setup() 37 | defer teardown() 38 | 39 | httpmock.RegisterResponder( 40 | "GET", 41 | "https://fooshop.myshopify.com/admin/blogs/count.json", 42 | httpmock.NewStringResponder( 43 | 200, 44 | `{"count": 5}`, 45 | ), 46 | ) 47 | 48 | cnt, err := client.Blog.Count(nil) 49 | if err != nil { 50 | t.Errorf("Blog.Count returned error: %v", err) 51 | } 52 | 53 | expected := 5 54 | if cnt != expected { 55 | t.Errorf("Blog.Count returned %d, expected %d", cnt, expected) 56 | } 57 | 58 | } 59 | 60 | func TestBlogGet(t *testing.T) { 61 | setup() 62 | defer teardown() 63 | 64 | httpmock.RegisterResponder( 65 | "GET", 66 | "https://fooshop.myshopify.com/admin/blogs/1.json", 67 | httpmock.NewStringResponder( 68 | 200, 69 | `{"blog": {"id":1}}`, 70 | ), 71 | ) 72 | 73 | blog, err := client.Blog.Get(1, nil) 74 | if err != nil { 75 | t.Errorf("Blog.Get returned error: %v", err) 76 | } 77 | 78 | expected := &Blog{ID: 1} 79 | if !reflect.DeepEqual(blog, expected) { 80 | t.Errorf("Blog.Get returned %+v, expected %+v", blog, expected) 81 | } 82 | 83 | } 84 | 85 | func TestBlogCreate(t *testing.T) { 86 | setup() 87 | defer teardown() 88 | 89 | httpmock.RegisterResponder( 90 | "POST", 91 | "https://fooshop.myshopify.com/admin/blogs.json", 92 | httpmock.NewBytesResponder( 93 | 200, 94 | loadFixture("blog.json"), 95 | ), 96 | ) 97 | 98 | blog := Blog{ 99 | Title: "Mah Blog", 100 | } 101 | 102 | returnedBlog, err := client.Blog.Create(blog) 103 | if err != nil { 104 | t.Errorf("Blog.Create returned error: %v", err) 105 | } 106 | 107 | expectedInt := 241253187 108 | if returnedBlog.ID != expectedInt { 109 | t.Errorf("Blog.ID returned %+v, expected %+v", returnedBlog.ID, expectedInt) 110 | } 111 | 112 | } 113 | 114 | func TestBlogUpdate(t *testing.T) { 115 | setup() 116 | defer teardown() 117 | 118 | httpmock.RegisterResponder( 119 | "PUT", 120 | "https://fooshop.myshopify.com/admin/blogs/1.json", 121 | httpmock.NewBytesResponder( 122 | 200, 123 | loadFixture("blog.json"), 124 | ), 125 | ) 126 | 127 | blog := Blog{ 128 | ID: 1, 129 | Title: "Mah Blog", 130 | } 131 | 132 | returnedBlog, err := client.Blog.Update(blog) 133 | if err != nil { 134 | t.Errorf("Blog.Update returned error: %v", err) 135 | } 136 | 137 | expectedInt := 241253187 138 | if returnedBlog.ID != expectedInt { 139 | t.Errorf("Blog.ID returned %+v, expected %+v", returnedBlog.ID, expectedInt) 140 | } 141 | } 142 | 143 | func TestBlogDelete(t *testing.T) { 144 | setup() 145 | defer teardown() 146 | 147 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/blogs/1.json", 148 | httpmock.NewStringResponder(200, "{}")) 149 | 150 | err := client.Blog.Delete(1) 151 | if err != nil { 152 | t.Errorf("Blog.Delete returned error: %v", err) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /customcollection.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const customCollectionsBasePath = "admin/custom_collections" 9 | const customCollectionsResourceName = "collections" 10 | 11 | // CustomCollectionService is an interface for interacting with the custom 12 | // collection endpoints of the Shopify API. 13 | // See https://help.shopify.com/api/reference/customcollection 14 | type CustomCollectionService interface { 15 | List(interface{}) ([]CustomCollection, error) 16 | Count(interface{}) (int, error) 17 | Get(int, interface{}) (*CustomCollection, error) 18 | Create(CustomCollection) (*CustomCollection, error) 19 | Update(CustomCollection) (*CustomCollection, error) 20 | Delete(int) error 21 | 22 | // MetafieldsService used for CustomCollection resource to communicate with Metafields resource 23 | MetafieldsService 24 | } 25 | 26 | // CustomCollectionServiceOp handles communication with the custom collection 27 | // related methods of the Shopify API. 28 | type CustomCollectionServiceOp struct { 29 | client *Client 30 | } 31 | 32 | // CustomCollection represents a Shopify custom collection. 33 | type CustomCollection struct { 34 | ID int `json:"id"` 35 | Handle string `json:"handle"` 36 | Title string `json:"title"` 37 | UpdatedAt *time.Time `json:"updated_at"` 38 | BodyHTML string `json:"body_html"` 39 | SortOrder string `json:"sort_order"` 40 | TemplateSuffix string `json:"template_suffix"` 41 | Image Image `json:"image"` 42 | Published bool `json:"published"` 43 | PublishedAt *time.Time `json:"published_at"` 44 | PublishedScope string `json:"published_scope"` 45 | Metafields []Metafield `json:"metafields,omitempty"` 46 | } 47 | 48 | // CustomCollectionResource represents the result form the custom_collections/X.json endpoint 49 | type CustomCollectionResource struct { 50 | Collection *CustomCollection `json:"custom_collection"` 51 | } 52 | 53 | // CustomCollectionsResource represents the result from the custom_collections.json endpoint 54 | type CustomCollectionsResource struct { 55 | Collections []CustomCollection `json:"custom_collections"` 56 | } 57 | 58 | // List custom collections 59 | func (s *CustomCollectionServiceOp) List(options interface{}) ([]CustomCollection, error) { 60 | path := fmt.Sprintf("%s.json", customCollectionsBasePath) 61 | resource := new(CustomCollectionsResource) 62 | err := s.client.Get(path, resource, options) 63 | return resource.Collections, err 64 | } 65 | 66 | // Count custom collections 67 | func (s *CustomCollectionServiceOp) Count(options interface{}) (int, error) { 68 | path := fmt.Sprintf("%s/count.json", customCollectionsBasePath) 69 | return s.client.Count(path, options) 70 | } 71 | 72 | // Get individual custom collection 73 | func (s *CustomCollectionServiceOp) Get(collectionID int, options interface{}) (*CustomCollection, error) { 74 | path := fmt.Sprintf("%s/%d.json", customCollectionsBasePath, collectionID) 75 | resource := new(CustomCollectionResource) 76 | err := s.client.Get(path, resource, options) 77 | return resource.Collection, err 78 | } 79 | 80 | // Create a new custom collection 81 | // See Image for the details of the Image creation for a collection. 82 | func (s *CustomCollectionServiceOp) Create(collection CustomCollection) (*CustomCollection, error) { 83 | path := fmt.Sprintf("%s.json", customCollectionsBasePath) 84 | wrappedData := CustomCollectionResource{Collection: &collection} 85 | resource := new(CustomCollectionResource) 86 | err := s.client.Post(path, wrappedData, resource) 87 | return resource.Collection, err 88 | } 89 | 90 | // Update an existing custom collection 91 | func (s *CustomCollectionServiceOp) Update(collection CustomCollection) (*CustomCollection, error) { 92 | path := fmt.Sprintf("%s/%d.json", customCollectionsBasePath, collection.ID) 93 | wrappedData := CustomCollectionResource{Collection: &collection} 94 | resource := new(CustomCollectionResource) 95 | err := s.client.Put(path, wrappedData, resource) 96 | return resource.Collection, err 97 | } 98 | 99 | // Delete an existing custom collection. 100 | func (s *CustomCollectionServiceOp) Delete(collectionID int) error { 101 | return s.client.Delete(fmt.Sprintf("%s/%d.json", customCollectionsBasePath, collectionID)) 102 | } 103 | 104 | // List metafields for a custom collection 105 | func (s *CustomCollectionServiceOp) ListMetafields(customCollectionID int, options interface{}) ([]Metafield, error) { 106 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customCollectionsResourceName, resourceID: customCollectionID} 107 | return metafieldService.List(options) 108 | } 109 | 110 | // Count metafields for a custom collection 111 | func (s *CustomCollectionServiceOp) CountMetafields(customCollectionID int, options interface{}) (int, error) { 112 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customCollectionsResourceName, resourceID: customCollectionID} 113 | return metafieldService.Count(options) 114 | } 115 | 116 | // Get individual metafield for a custom collection 117 | func (s *CustomCollectionServiceOp) GetMetafield(customCollectionID int, metafieldID int, options interface{}) (*Metafield, error) { 118 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customCollectionsResourceName, resourceID: customCollectionID} 119 | return metafieldService.Get(metafieldID, options) 120 | } 121 | 122 | // Create a new metafield for a custom collection 123 | func (s *CustomCollectionServiceOp) CreateMetafield(customCollectionID int, metafield Metafield) (*Metafield, error) { 124 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customCollectionsResourceName, resourceID: customCollectionID} 125 | return metafieldService.Create(metafield) 126 | } 127 | 128 | // Update an existing metafield for a custom collection 129 | func (s *CustomCollectionServiceOp) UpdateMetafield(customCollectionID int, metafield Metafield) (*Metafield, error) { 130 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customCollectionsResourceName, resourceID: customCollectionID} 131 | return metafieldService.Update(metafield) 132 | } 133 | 134 | // // Delete an existing metafield for a custom collection 135 | func (s *CustomCollectionServiceOp) DeleteMetafield(customCollectionID int, metafieldID int) error { 136 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customCollectionsResourceName, resourceID: customCollectionID} 137 | return metafieldService.Delete(metafieldID) 138 | } 139 | -------------------------------------------------------------------------------- /customer.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | const customersBasePath = "admin/customers" 11 | const customersResourceName = "customers" 12 | 13 | // CustomerService is an interface for interfacing with the customers endpoints 14 | // of the Shopify API. 15 | // See: https://help.shopify.com/api/reference/customer 16 | type CustomerService interface { 17 | List(interface{}) ([]Customer, error) 18 | Count(interface{}) (int, error) 19 | Get(int, interface{}) (*Customer, error) 20 | Search(interface{}) ([]Customer, error) 21 | Create(Customer) (*Customer, error) 22 | Update(Customer) (*Customer, error) 23 | Delete(int) error 24 | 25 | // MetafieldsService used for Customer resource to communicate with Metafields resource 26 | MetafieldsService 27 | } 28 | 29 | // CustomerServiceOp handles communication with the product related methods of 30 | // the Shopify API. 31 | type CustomerServiceOp struct { 32 | client *Client 33 | } 34 | 35 | // Customer represents a Shopify customer 36 | type Customer struct { 37 | ID int `json:"id,omitempty"` 38 | Email string `json:"email,omitempty"` 39 | FirstName string `json:"first_name,omitempty"` 40 | LastName string `json:"last_name,omitempty"` 41 | State string `json:"state,omitempty"` 42 | Note string `json:"note,omitempty"` 43 | VerifiedEmail bool `json:"verified_email,omitempty"` 44 | MultipassIdentifier string `json:"multipass_identifier,omitempty"` 45 | OrdersCount int `json:"orders_count,omitempty"` 46 | TaxExempt bool `json:"tax_exempt,omitempty"` 47 | TotalSpent *decimal.Decimal `json:"total_spent,omitempty"` 48 | Phone string `json:"phone,omitempty"` 49 | Tags string `json:"tags,omitempty"` 50 | LastOrderId int `json:"last_order_id,omitempty"` 51 | LastOrderName string `json:"last_order_name,omitempty"` 52 | AcceptsMarketing bool `json:"accepts_marketing,omitempty"` 53 | DefaultAddress *CustomerAddress `json:"default_address,omitempty"` 54 | Addresses []*CustomerAddress `json:"addresses,omitempty"` 55 | CreatedAt *time.Time `json:"created_at,omitempty"` 56 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 57 | Metafields []Metafield `json:"metafields,omitempty"` 58 | } 59 | 60 | // Represents the result from the customers/X.json endpoint 61 | type CustomerResource struct { 62 | Customer *Customer `json:"customer"` 63 | } 64 | 65 | // Represents the result from the customers.json endpoint 66 | type CustomersResource struct { 67 | Customers []Customer `json:"customers"` 68 | } 69 | 70 | // Represents the options available when searching for a customer 71 | type CustomerSearchOptions struct { 72 | Page int `url:"page,omitempty"` 73 | Limit int `url:"limit,omitempty"` 74 | Fields string `url:"fields,omitempty"` 75 | Order string `url:"order,omitempty"` 76 | Query string `url:"query,omitempty"` 77 | } 78 | 79 | // List customers 80 | func (s *CustomerServiceOp) List(options interface{}) ([]Customer, error) { 81 | path := fmt.Sprintf("%s.json", customersBasePath) 82 | resource := new(CustomersResource) 83 | err := s.client.Get(path, resource, options) 84 | return resource.Customers, err 85 | } 86 | 87 | // Count customers 88 | func (s *CustomerServiceOp) Count(options interface{}) (int, error) { 89 | path := fmt.Sprintf("%s/count.json", customersBasePath) 90 | return s.client.Count(path, options) 91 | } 92 | 93 | // Get customer 94 | func (s *CustomerServiceOp) Get(customerID int, options interface{}) (*Customer, error) { 95 | path := fmt.Sprintf("%s/%v.json", customersBasePath, customerID) 96 | resource := new(CustomerResource) 97 | err := s.client.Get(path, resource, options) 98 | return resource.Customer, err 99 | } 100 | 101 | // Create a new customer 102 | func (s *CustomerServiceOp) Create(customer Customer) (*Customer, error) { 103 | path := fmt.Sprintf("%s.json", customersBasePath) 104 | wrappedData := CustomerResource{Customer: &customer} 105 | resource := new(CustomerResource) 106 | err := s.client.Post(path, wrappedData, resource) 107 | return resource.Customer, err 108 | } 109 | 110 | // Update an existing customer 111 | func (s *CustomerServiceOp) Update(customer Customer) (*Customer, error) { 112 | path := fmt.Sprintf("%s/%d.json", customersBasePath, customer.ID) 113 | wrappedData := CustomerResource{Customer: &customer} 114 | resource := new(CustomerResource) 115 | err := s.client.Put(path, wrappedData, resource) 116 | return resource.Customer, err 117 | } 118 | 119 | // Delete an existing customer 120 | func (s *CustomerServiceOp) Delete(customerID int) error { 121 | path := fmt.Sprintf("%s/%d.json", customersBasePath, customerID) 122 | return s.client.Delete(path) 123 | } 124 | 125 | // Search customers 126 | func (s *CustomerServiceOp) Search(options interface{}) ([]Customer, error) { 127 | path := fmt.Sprintf("%s/search.json", customersBasePath) 128 | resource := new(CustomersResource) 129 | err := s.client.Get(path, resource, options) 130 | return resource.Customers, err 131 | } 132 | 133 | // List metafields for a customer 134 | func (s *CustomerServiceOp) ListMetafields(customerID int, options interface{}) ([]Metafield, error) { 135 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customersResourceName, resourceID: customerID} 136 | return metafieldService.List(options) 137 | } 138 | 139 | // Count metafields for a customer 140 | func (s *CustomerServiceOp) CountMetafields(customerID int, options interface{}) (int, error) { 141 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customersResourceName, resourceID: customerID} 142 | return metafieldService.Count(options) 143 | } 144 | 145 | // Get individual metafield for a customer 146 | func (s *CustomerServiceOp) GetMetafield(customerID int, metafieldID int, options interface{}) (*Metafield, error) { 147 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customersResourceName, resourceID: customerID} 148 | return metafieldService.Get(metafieldID, options) 149 | } 150 | 151 | // Create a new metafield for a customer 152 | func (s *CustomerServiceOp) CreateMetafield(customerID int, metafield Metafield) (*Metafield, error) { 153 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customersResourceName, resourceID: customerID} 154 | return metafieldService.Create(metafield) 155 | } 156 | 157 | // Update an existing metafield for a customer 158 | func (s *CustomerServiceOp) UpdateMetafield(customerID int, metafield Metafield) (*Metafield, error) { 159 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customersResourceName, resourceID: customerID} 160 | return metafieldService.Update(metafield) 161 | } 162 | 163 | // // Delete an existing metafield for a customer 164 | func (s *CustomerServiceOp) DeleteMetafield(customerID int, metafieldID int) error { 165 | metafieldService := &MetafieldServiceOp{client: s.client, resource: customersResourceName, resourceID: customerID} 166 | return metafieldService.Delete(metafieldID) 167 | } 168 | -------------------------------------------------------------------------------- /customer_address.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | // CustomerAddress represents a Shopify customer address 4 | type CustomerAddress struct { 5 | ID int `json:"id,omitempty"` 6 | CustomerID int `json:"customer_id,omitempty"` 7 | FirstName string `json:"first_name"` 8 | LastName string `json:"last_name"` 9 | Company string `json:"company"` 10 | Address1 string `json:"address1"` 11 | Address2 string `json:"address2"` 12 | City string `json:"city"` 13 | Province string `json:"province"` 14 | Country string `json:"country"` 15 | Zip string `json:"zip"` 16 | Phone string `json:"phone"` 17 | Name string `json:"name"` 18 | ProvinceCode string `json:"province_code"` 19 | CountryCode string `json:"country_code"` 20 | CountryName string `json:"country_name"` 21 | Default bool `json:"default"` 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | dev: 5 | build: . 6 | command: go test -v -cover ./... 7 | volumes: 8 | - .:/go/src/github.com/getconversio/go-shopify 9 | -------------------------------------------------------------------------------- /fixtures/applicationcharge.json: -------------------------------------------------------------------------------- 1 | { 2 | "application_charge": { 3 | "id": 1017262355, 4 | "name": "Super Duper Expensive action", 5 | "api_client_id": 755357713, 6 | "price": "100.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "test": null, 10 | "created_at": "2018-07-05T13:11:28-04:00", 11 | "updated_at": "2018-07-05T13:11:28-04:00", 12 | "charge_type": null, 13 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1017262355", 14 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1017262355/confirm_application_charge?signature=BAhpBBMxojw%3D--1139a82a3433b1a6771786e03f02300440e11883" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /fixtures/asset.json: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "key": "templates\/index.liquid", 4 | "public_url": null, 5 | "created_at": "2010-07-12T15:31:50-04:00", 6 | "updated_at": "2017-01-05T15:38:16-05:00", 7 | "content_type": "text\/x-liquid", 8 | "size": 110, 9 | "theme_id": 1 10 | } 11 | } -------------------------------------------------------------------------------- /fixtures/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "blog": { 3 | "id": 241253187, 4 | "handle": "apple-blog", 5 | "title": "Mah Blog", 6 | "updated_at": "2006-02-01T19:00:00-05:00", 7 | "commentable": "no", 8 | "feedburner": null, 9 | "feedburner_location": null, 10 | "created_at": "2018-05-07T15:33:38-04:00", 11 | "template_suffix": null, 12 | "tags": "Announcing, Mystery" 13 | } 14 | } -------------------------------------------------------------------------------- /fixtures/customcollection.json: -------------------------------------------------------------------------------- 1 | {"custom_collection":{"id":30497275952,"handle":"macbooks","title":"Macbooks","updated_at":"2018-02-06T02:20:25-05:00","body_html":"Macbook Body","published_at":"2018-02-06T02:20:25-05:00","sort_order":"best-selling","template_suffix":null,"published_scope":"web"}} 2 | -------------------------------------------------------------------------------- /fixtures/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "customer": { 3 | "id": 1, 4 | "email": "test@example.com", 5 | "accepts_marketing": true, 6 | "created_at": "2017-09-23T18:15:47+10:00", 7 | "updated_at": "2017-10-17T15:51:51+10:00", 8 | "first_name": "Test", 9 | "last_name": "Citizen", 10 | "orders_count": 4, 11 | "state": "enabled", 12 | "total_spent": "278.60", 13 | "last_order_id": 123, 14 | "note": null, 15 | "verified_email": true, 16 | "multipass_identifier": null, 17 | "tax_exempt": false, 18 | "phone": null, 19 | "tags": "tag1,tag2", 20 | "last_order_name": "#1234", 21 | "addresses": [ 22 | { 23 | "id": 1, 24 | "customer_id": 1, 25 | "first_name": "Test", 26 | "last_name": "Citizen", 27 | "company": null, 28 | "address1": "1 Smith St", 29 | "address2": null, 30 | "city": "BRISBANE", 31 | "province": "Queensland", 32 | "country": "Australia", 33 | "zip": "4000", 34 | "phone": "1111 111 111", 35 | "name": "Test Citizen", 36 | "province_code": "QLD", 37 | "country_code": "AU", 38 | "country_name": "Australia", 39 | "default": true 40 | } 41 | ], 42 | "default_address": { 43 | "id": 1, 44 | "customer_id": 1, 45 | "first_name": "Test", 46 | "last_name": "Citizen", 47 | "company": null, 48 | "address1": "1 Smith St", 49 | "address2": null, 50 | "city": "BRISBANE", 51 | "province": "Queensland", 52 | "country": "Australia", 53 | "zip": "4000", 54 | "phone": "1111 111 111", 55 | "name": "Test Citizen", 56 | "province_code": "QLD", 57 | "country_code": "AU", 58 | "country_name": "Australia", 59 | "default": true 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /fixtures/fulfillment.json: -------------------------------------------------------------------------------- 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}]}]}} 2 | -------------------------------------------------------------------------------- /fixtures/image.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": { 3 | "id": 1, 4 | "product_id": 1, 5 | "position": 1, 6 | "created_at": "2017-07-24T19:09:43-00:00", 7 | "updated_at": "2017-07-24T19:09:43-00:00", 8 | "width": 123, 9 | "height": 456, 10 | "src": "https:\/\/cdn.shopify.com\/s\/files\/1\/0006\/9093\/3842\/products\/ipod-nano.png?v=1500937783", 11 | "variant_ids": [ 12 | 808950810, 13 | 808950811 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /fixtures/images.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "id": 1, 5 | "product_id": 1, 6 | "position": 1, 7 | "created_at": "2017-07-24T19:09:43-00:00", 8 | "updated_at": "2017-07-24T19:09:43-00:00", 9 | "width": 123, 10 | "height": 456, 11 | "src": "https:\/\/cdn.shopify.com\/s\/files\/1\/0006\/9093\/3842\/products\/ipod-nano.png?v=1500937783", 12 | "variant_ids": [ 13 | 808950810, 14 | 808950811 15 | ] 16 | }, 17 | { 18 | "id": 2, 19 | "product_id": 1, 20 | "position": 2, 21 | "created_at": "2017-07-24T19:09:43-04:00", 22 | "updated_at": "2017-07-24T19:09:43-04:00", 23 | "width": 123, 24 | "height": 456, 25 | "src": "https:\/\/cdn.shopify.com\/s\/files\/1\/0006\/9093\/3842\/products\/ipod-nano-2.png?v=1500937783", 26 | "variant_ids": [ 27 | ] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /fixtures/metafield.json: -------------------------------------------------------------------------------- 1 | { 2 | "metafield": { 3 | "id": 721389482, 4 | "namespace": "affiliates", 5 | "key": "app_key", 6 | "value": "app_value", 7 | "value_type": "string", 8 | "description": null, 9 | "owner_id": 690933842, 10 | "created_at": "2018-04-27T15:15:25-04:00", 11 | "updated_at": "2018-04-27T15:15:25-04:00", 12 | "owner_resource": "shop" 13 | } 14 | } -------------------------------------------------------------------------------- /fixtures/order.json: -------------------------------------------------------------------------------- 1 | {"order":{"id":123456,"email":"jon@doe.ca","closed_at":null,"created_at":"2016-05-17T04:14:36-00:00","updated_at":"2016-05-17T04:14:36-04:00","number":234,"note":null,"token":null,"gateway":null,"test":true,"total_price":"10.00","subtotal_price":"0.00","total_weight":0,"total_tax":null,"taxes_included":false,"currency":"USD","financial_status":"voided","confirmed":false,"total_discounts":"5.00","total_line_items_price":"5.00","cart_token":null,"buyer_accepts_marketing":true,"name":"#9999","referring_site":null,"landing_site":null,"cancelled_at":"2016-05-17T04:14:36-04:00","cancel_reason":"customer","total_price_usd":null,"checkout_token":null,"reference":null,"user_id":null,"location_id":null,"source_identifier":null,"source_url":null,"processed_at":null,"device_id":null,"browser_ip":null,"landing_site_ref":null,"order_number":1234,"discount_codes":[],"note_attributes":[],"payment_gateway_names":["visa","bogus"],"processing_method":"","checkout_id":null,"source_name":"web","fulfillment_status":"pending","tax_lines":[],"tags":"","contact_email":"jon@doe.ca","order_status_url":null,"line_items":[{"id":254721536,"variant_id":null,"title":"Soda","quantity":1,"price":"0.00","grams":0,"sku":"","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":111475476,"requires_shipping":true,"taxable":true,"gift_card":false,"pre_tax_price":"9.00","name":"Soda","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"total_discount":"0.00","fulfillment_status":null,"tax_lines":[]},{"id":5,"variant_id":null,"title":"Another Beer For Good Times","quantity":1,"price":"5.00","grams":500,"sku":"","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":5410685889,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"Another Beer For Good Times","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"total_discount":"5.00","fulfillment_status":null,"tax_lines":[]}],"shipping_lines":[{"id":null,"title":"Generic Shipping","price":"10.00","code":null,"source":"shopify","phone":null,"carrier_identifier":null,"tax_lines":[]}],"billing_address":{"first_name":"Bob","address1":"123 Billing Street","phone":"555-555-BILL","city":"Billtown","zip":"K2P0B0","province":"Kentucky","country":"United States","last_name":"Biller","address2":null,"company":"My Company","latitude":null,"longitude":null,"name":"Bob Biller","country_code":"US","province_code":"KY"},"shipping_address":{"first_name":"Steve","address1":"123 Shipping Street","phone":"555-555-SHIP","city":"Shippington","zip":"K2P0S0","province":"Kentucky","country":"United States","last_name":"Shipper","address2":null,"company":"Shipping Company","latitude":null,"longitude":null,"name":"Steve Shipper","country_code":"US","province_code":"KY"},"fulfillments":[],"refunds":[],"customer":{"id":null,"email":"john@test.com","accepts_marketing":false,"created_at":null,"updated_at":null,"first_name":"John","last_name":"Smith","orders_count":0,"state":"disabled","total_spent":"0.00","last_order_id":null,"note":null,"verified_email":true,"multipass_identifier":null,"tax_exempt":false,"tags":"","last_order_name":null,"default_address":{"id":null,"first_name":null,"last_name":null,"company":null,"address1":"123 Elm St.","address2":null,"city":"Ottawa","province":"Ontario","country":"Canada","zip":"K2H7A8","phone":"123-123-1234","name":"","province_code":"ON","country_code":"CA","country_name":"Canada","default":true}}}} 2 | -------------------------------------------------------------------------------- /fixtures/order_with_transaction.json: -------------------------------------------------------------------------------- 1 | {"order":{"id":123456,"email":"jon@doe.ca","closed_at":null,"created_at":"2016-05-17T04:14:36-00:00","updated_at":"2016-05-17T04:14:36-04:00","number":234,"note":null,"token":null,"gateway":null,"test":true,"total_price":"10.00","subtotal_price":"0.00","total_weight":0,"total_tax":null,"taxes_included":false,"currency":"USD","financial_status":"voided","confirmed":false,"total_discounts":"5.00","total_line_items_price":"5.00","cart_token":null,"buyer_accepts_marketing":true,"name":"#9999","referring_site":null,"landing_site":null,"cancelled_at":"2016-05-17T04:14:36-04:00","cancel_reason":"customer","total_price_usd":null,"checkout_token":null,"reference":null,"user_id":null,"location_id":null,"source_identifier":null,"source_url":null,"processed_at":null,"device_id":null,"browser_ip":null,"landing_site_ref":null,"order_number":1234,"discount_codes":[],"note_attributes":[],"payment_gateway_names":["visa","bogus"],"processing_method":"","checkout_id":null,"source_name":"web","fulfillment_status":"pending","tax_lines":[],"tags":"","contact_email":"jon@doe.ca","order_status_url":null,"line_items":[{"id":254721536,"variant_id":null,"title":"Soda","quantity":1,"price":"0.00","grams":0,"sku":"","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":111475476,"requires_shipping":true,"taxable":true,"gift_card":false,"pre_tax_price":"9.00","name":"Soda","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"total_discount":"0.00","fulfillment_status":null,"tax_lines":[]},{"id":5,"variant_id":null,"title":"Another Beer For Good Times","quantity":1,"price":"5.00","grams":500,"sku":"","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":5410685889,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"Another Beer For Good Times","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"total_discount":"5.00","fulfillment_status":null,"tax_lines":[]}],"shipping_lines":[{"id":null,"title":"Generic Shipping","price":"10.00","code":null,"source":"shopify","phone":null,"carrier_identifier":null,"tax_lines":[]}],"billing_address":{"first_name":"Bob","address1":"123 Billing Street","phone":"555-555-BILL","city":"Billtown","zip":"K2P0B0","province":"Kentucky","country":"United States","last_name":"Biller","address2":null,"company":"My Company","latitude":null,"longitude":null,"name":"Bob Biller","country_code":"US","province_code":"KY"},"shipping_address":{"first_name":"Steve","address1":"123 Shipping Street","phone":"555-555-SHIP","city":"Shippington","zip":"K2P0S0","province":"Kentucky","country":"United States","last_name":"Shipper","address2":null,"company":"Shipping Company","latitude":null,"longitude":null,"name":"Steve Shipper","country_code":"US","province_code":"KY"},"fulfillments":[],"refunds":[],"customer":{"id":null,"email":"john@test.com","accepts_marketing":false,"created_at":null,"updated_at":null,"first_name":"John","last_name":"Smith","orders_count":0,"state":"disabled","total_spent":"0.00","last_order_id":null,"note":null,"verified_email":true,"multipass_identifier":null,"tax_exempt":false,"tags":"","last_order_name":null,"default_address":{"id":null,"first_name":null,"last_name":null,"company":null,"address1":"123 Elm St.","address2":null,"city":"Ottawa","province":"Ontario","country":"Canada","zip":"K2H7A8","phone":"123-123-1234","name":"","province_code":"ON","country_code":"CA","country_name":"Canada","default":true}},"transactions":[{"id":1,"order_id":123456,"amount":"79.60","kind":"sale","gateway":"mygateway","status":"success","message":"Approved","created_at":"2017-10-09T19:26:23+00:00","test":false,"authorization":"ABC123","currency":"AUD","location_id":null,"user_id":null,"parent_id":null,"device_id":null,"receipt":{"vendor":"myshop","partner":"paypal","result":"0","avs_result":"X","rrn":"abcd1234","message":"Approved","pn_ref":"AAAABBBB","transactionid":"abc1234"},"error_code":null,"source_name":"web","payment_details":{"credit_card_bin":"123456","avs_result_code":"X","cvv_result_code":null,"credit_card_number":"•••• •••• •••• 1234","credit_card_company":"Mastercard"}}]}} 2 | -------------------------------------------------------------------------------- /fixtures/orders.json: -------------------------------------------------------------------------------- 1 | {"orders":[{"id":123456,"email":"jon@doe.ca","closed_at":null,"created_at":"2016-05-17T04:14:36-00:00","updated_at":"2016-05-17T04:14:36-04:00","number":234,"note":null,"token":null,"gateway":null,"test":true,"total_price":"10.00","subtotal_price":"0.00","total_weight":0,"total_tax":null,"taxes_included":false,"currency":"USD","financial_status":"voided","confirmed":false,"total_discounts":"5.00","total_line_items_price":"5.00","cart_token":null,"buyer_accepts_marketing":true,"name":"#9999","referring_site":null,"landing_site":null,"cancelled_at":"2016-05-17T04:14:36-04:00","cancel_reason":"customer","total_price_usd":null,"checkout_token":null,"reference":null,"user_id":null,"location_id":null,"source_identifier":null,"source_url":null,"processed_at":null,"device_id":null,"browser_ip":null,"landing_site_ref":null,"order_number":1234,"discount_codes":[],"note_attributes":[],"payment_gateway_names":["visa","bogus"],"processing_method":"","checkout_id":null,"source_name":"web","fulfillment_status":"pending","tax_lines":[],"tags":"","contact_email":"jon@doe.ca","order_status_url":null,"line_items":[{"id":254721536,"variant_id":null,"title":"Soda","quantity":1,"price":"0.00","grams":0,"sku":"","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":111475476,"requires_shipping":true,"taxable":true,"gift_card":false,"pre_tax_price":"9.00","name":"Soda","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"total_discount":"0.00","fulfillment_status":null,"tax_lines":[]},{"id":5,"variant_id":null,"title":"Another Beer For Good Times","quantity":1,"price":"5.00","grams":500,"sku":"","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":5410685889,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"Another Beer For Good Times","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"total_discount":"5.00","fulfillment_status":null,"tax_lines":[]}],"shipping_lines":[{"id":null,"title":"Generic Shipping","price":"10.00","code":null,"source":"shopify","phone":null,"carrier_identifier":null,"tax_lines":[]}],"billing_address":{"first_name":"Bob","address1":"123 Billing Street","phone":"555-555-BILL","city":"Billtown","zip":"K2P0B0","province":"Kentucky","country":"United States","last_name":"Biller","address2":null,"company":"My Company","latitude":null,"longitude":null,"name":"Bob Biller","country_code":"US","province_code":"KY"},"shipping_address":{"first_name":"Steve","address1":"123 Shipping Street","phone":"555-555-SHIP","city":"Shippington","zip":"K2P0S0","province":"Kentucky","country":"United States","last_name":"Shipper","address2":null,"company":"Shipping Company","latitude":null,"longitude":null,"name":"Steve Shipper","country_code":"US","province_code":"KY"},"fulfillments":[],"refunds":[],"customer":{"id":null,"email":"john@test.com","accepts_marketing":false,"created_at":null,"updated_at":null,"first_name":"John","last_name":"Smith","orders_count":0,"state":"disabled","total_spent":"0.00","last_order_id":null,"note":null,"verified_email":true,"multipass_identifier":null,"tax_exempt":false,"tags":"","last_order_name":null,"default_address":{"id":null,"first_name":null,"last_name":null,"company":null,"address1":"123 Elm St.","address2":null,"city":"Ottawa","province":"Ontario","country":"Canada","zip":"K2H7A8","phone":"123-123-1234","name":"","province_code":"ON","country_code":"CA","country_name":"Canada","default":true}}}]} 2 | -------------------------------------------------------------------------------- /fixtures/page.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "id": 1, 4 | "author": "Unknown", 5 | "body_html": "NOT FOUND<\/strong>", 6 | "handle": "404", 7 | "template_suffix": "notfound", 8 | "title": "404" 9 | } 10 | } -------------------------------------------------------------------------------- /fixtures/product.json: -------------------------------------------------------------------------------- 1 | { 2 | "product": { 3 | "id": 1071559748, 4 | "title": "Burton Custom Freestyle 151", 5 | "body_html": "Good snowboard!<\/strong>", 6 | "vendor": "Burton", 7 | "product_type": "Snowboard", 8 | "created_at": "2017-09-22T14:48:44-04:00", 9 | "handle": "burton-custom-freestyle-151", 10 | "updated_at": "2017-09-22T14:48:44-04:00", 11 | "published_at": "2017-09-22T14:48:44-04:00", 12 | "template_suffix": null, 13 | "published_scope": "global", 14 | "tags": "\"Big Air\", Barnes & Noble, John's Fav", 15 | "variants": [ 16 | { 17 | "id": 1070325219, 18 | "product_id": 1071559748, 19 | "title": "Default Title", 20 | "price": "0.00", 21 | "sku": "", 22 | "position": 1, 23 | "grams": 0, 24 | "inventory_policy": "deny", 25 | "compare_at_price": null, 26 | "fulfillment_service": "manual", 27 | "inventory_management": null, 28 | "option1": "Default Title", 29 | "option2": null, 30 | "option3": null, 31 | "created_at": "2017-09-22T14:48:44-04:00", 32 | "updated_at": "2017-09-22T14:48:44-04:00", 33 | "taxable": true, 34 | "barcode": null, 35 | "image_id": null, 36 | "inventory_quantity": 1, 37 | "weight": 0.0, 38 | "weight_unit": "lb", 39 | "old_inventory_quantity": 1, 40 | "requires_shipping": true 41 | } 42 | ], 43 | "options": [ 44 | { 45 | "id": 1022828904, 46 | "product_id": 1071559748, 47 | "name": "Title", 48 | "position": 1, 49 | "values": [ 50 | "Default Title" 51 | ] 52 | } 53 | ], 54 | "images": [ 55 | ], 56 | "image": null 57 | } 58 | } -------------------------------------------------------------------------------- /fixtures/reccuringapplicationcharge/reccuringapplicationcharge.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurring_application_charge": { 3 | "id": 1029266948, 4 | "name": "Super Duper Plan", 5 | "api_client_id": 755357713, 6 | "price": "10.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "billing_on": null, 10 | "created_at": "2018-05-07T15:47:10-04:00", 11 | "updated_at": "2018-05-07T15:47:10-04:00", 12 | "test": null, 13 | "activated_on": null, 14 | "trial_ends_on": null, 15 | "cancelled_on": null, 16 | "trial_days": 0, 17 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1029266948", 18 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1029266948/confirm_recurring_application_charge?signature=BAhpBAReWT0%3D--b51a6db06a3792c4439783fcf0f2e89bf1c9df68" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/reccuringapplicationcharge/reccuringapplicationcharge_all_fields_affected.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurring_application_charge": { 3 | "id": 1029266948, 4 | "name": "Super Duper Plan", 5 | "api_client_id": 755357713, 6 | "price": "10.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "billing_on": "2018-06-05", 10 | "created_at": "2018-06-05", 11 | "updated_at": "2018-06-05", 12 | "test": null, 13 | "activated_on": "2018-06-05", 14 | "trial_ends_on": "2018-06-05", 15 | "cancelled_on": "2018-06-05", 16 | "trial_days": 0, 17 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1029266948", 18 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1029266948/confirm_recurring_application_charge?signature=BAhpBAReWT0%3D--b51a6db06a3792c4439783fcf0f2e89bf1c9df68" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/reccuringapplicationcharge/reccuringapplicationcharge_bad.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurring_application_charge": { 3 | "id": 1029266948, 4 | "name": "Super Duper Plan", 5 | "api_client_id": 755357713, 6 | "price": "10.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "billing_on": false, 10 | "created_at": false, 11 | "updated_at": false, 12 | "test": null, 13 | "activated_on": false, 14 | "trial_ends_on": false, 15 | "cancelled_on": false, 16 | "trial_days": 0, 17 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1029266948", 18 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1029266948/confirm_recurring_application_charge?signature=BAhpBAReWT0%3D--b51a6db06a3792c4439783fcf0f2e89bf1c9df68" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/reccuringapplicationcharge/reccuringapplicationcharge_bad_activated_on.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurring_application_charge": { 3 | "id": 1029266948, 4 | "name": "Super Duper Plan", 5 | "api_client_id": 755357713, 6 | "price": "10.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "billing_on": null, 10 | "created_at": null, 11 | "updated_at": null, 12 | "test": null, 13 | "activated_on": "ptk 27 lip 14:24:13 2018 CEST", 14 | "trial_ends_on": null, 15 | "cancelled_on": null, 16 | "trial_days": 0, 17 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1029266948", 18 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1029266948/confirm_recurring_application_charge?signature=BAhpBAReWT0%3D--b51a6db06a3792c4439783fcf0f2e89bf1c9df68" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/reccuringapplicationcharge/reccuringapplicationcharge_bad_billing_on.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurring_application_charge": { 3 | "id": 1029266948, 4 | "name": "Super Duper Plan", 5 | "api_client_id": 755357713, 6 | "price": "10.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "billing_on": "ptk 27 lip 14:24:13 2018 CEST", 10 | "created_at": null, 11 | "updated_at": null, 12 | "test": null, 13 | "activated_on": null, 14 | "trial_ends_on": null, 15 | "cancelled_on": null, 16 | "trial_days": 0, 17 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1029266948", 18 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1029266948/confirm_recurring_application_charge?signature=BAhpBAReWT0%3D--b51a6db06a3792c4439783fcf0f2e89bf1c9df68" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/reccuringapplicationcharge/reccuringapplicationcharge_bad_cancelled_on.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurring_application_charge": { 3 | "id": 1029266948, 4 | "name": "Super Duper Plan", 5 | "api_client_id": 755357713, 6 | "price": "10.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "billing_on": null, 10 | "created_at": null, 11 | "updated_at": null, 12 | "test": null, 13 | "activated_on": null, 14 | "trial_ends_on": null, 15 | "cancelled_on": "ptk 27 lip 14:24:13 2018 CEST", 16 | "trial_days": 0, 17 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1029266948", 18 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1029266948/confirm_recurring_application_charge?signature=BAhpBAReWT0%3D--b51a6db06a3792c4439783fcf0f2e89bf1c9df68" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/reccuringapplicationcharge/reccuringapplicationcharge_bad_created_at.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurring_application_charge": { 3 | "id": 1029266948, 4 | "name": "Super Duper Plan", 5 | "api_client_id": 755357713, 6 | "price": "10.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "billing_on": null, 10 | "created_at": "ptk 27 lip 14:24:13 2018 CEST", 11 | "updated_at": null, 12 | "test": null, 13 | "activated_on": null, 14 | "trial_ends_on": null, 15 | "cancelled_on": null, 16 | "trial_days": 0, 17 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1029266948", 18 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1029266948/confirm_recurring_application_charge?signature=BAhpBAReWT0%3D--b51a6db06a3792c4439783fcf0f2e89bf1c9df68" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/reccuringapplicationcharge/reccuringapplicationcharge_bad_trial_ends_on.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurring_application_charge": { 3 | "id": 1029266948, 4 | "name": "Super Duper Plan", 5 | "api_client_id": 755357713, 6 | "price": "10.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "billing_on": null, 10 | "created_at": null, 11 | "updated_at": null, 12 | "test": null, 13 | "activated_on": null, 14 | "trial_ends_on": "ptk 27 lip 14:24:13 2018 CEST", 15 | "cancelled_on": null, 16 | "trial_days": 0, 17 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1029266948", 18 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1029266948/confirm_recurring_application_charge?signature=BAhpBAReWT0%3D--b51a6db06a3792c4439783fcf0f2e89bf1c9df68" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/reccuringapplicationcharge/reccuringapplicationcharge_bad_updated_at.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurring_application_charge": { 3 | "id": 1029266948, 4 | "name": "Super Duper Plan", 5 | "api_client_id": 755357713, 6 | "price": "10.00", 7 | "status": "pending", 8 | "return_url": "http://super-duper.shopifyapps.com/", 9 | "billing_on": null, 10 | "created_at": null, 11 | "updated_at": "ptk 27 lip 14:24:13 2018 CEST", 12 | "test": null, 13 | "activated_on": null, 14 | "trial_ends_on": null, 15 | "cancelled_on": null, 16 | "trial_days": 0, 17 | "decorated_return_url": "http://super-duper.shopifyapps.com/?charge_id=1029266948", 18 | "confirmation_url": "https://apple.myshopify.com/admin/charges/1029266948/confirm_recurring_application_charge?signature=BAhpBAReWT0%3D--b51a6db06a3792c4439783fcf0f2e89bf1c9df68" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/redirect.json: -------------------------------------------------------------------------------- 1 | { 2 | "redirect": { 3 | "id": 1, 4 | "path": "/from", 5 | "target": "/to" 6 | } 7 | } -------------------------------------------------------------------------------- /fixtures/script_tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "script_tag": { 3 | "id": 870402688, 4 | "src": "https://djavaskripped.org/fancy.js", 5 | "event": "onload", 6 | "created_at": "2018-03-21T11:39:52-04:00", 7 | "updated_at": "2018-03-21T11:39:52-04:00", 8 | "display_scope": "all" 9 | } 10 | } -------------------------------------------------------------------------------- /fixtures/shop.json: -------------------------------------------------------------------------------- 1 | { 2 | "shop": { 3 | "id": 690933842, 4 | "name": "Apple Computers", 5 | "email": "steve@apple.com", 6 | "domain": "shop.apple.com", 7 | "created_at": "2007-12-31T19:00:00+00:00", 8 | "province": "California", 9 | "country": "US", 10 | "address1": "1 Infinite Loop", 11 | "zip": "95014", 12 | "city": "Cupertino", 13 | "source": null, 14 | "phone": "1231231234", 15 | "updated_at": "2016-04-25T17:01:18-04:00", 16 | "customer_email": "customers@apple.com", 17 | "latitude": 45.45, 18 | "longitude": -75.43, 19 | "primary_location_id": null, 20 | "primary_locale": "en", 21 | "country_code": "US", 22 | "country_name": "United States", 23 | "currency": "USD", 24 | "timezone": "(GMT-05:00) Eastern Time (US & Canada)", 25 | "iana_timezone": "America\/New_York", 26 | "shop_owner": "Steve Jobs", 27 | "money_format": "${{amount}}", 28 | "money_with_currency_format": "${{amount}} USD", 29 | "province_code": "CA", 30 | "taxes_included": null, 31 | "tax_shipping": null, 32 | "county_taxes": true, 33 | "plan_display_name": "Shopify Plus", 34 | "plan_name": "enterprise", 35 | "has_discounts": false, 36 | "has_gift_cards": true, 37 | "myshopify_domain": "apple.myshopify.com", 38 | "google_apps_domain": null, 39 | "google_apps_login_enabled": null, 40 | "money_in_emails_format": "${{amount}}", 41 | "money_with_currency_in_emails_format": "${{amount}} USD", 42 | "eligible_for_payments": true, 43 | "requires_extra_payments_agreement": false, 44 | "password_enabled": false, 45 | "has_storefront": true, 46 | "eligible_for_card_reader_giveaway": false, 47 | "setup_required": false, 48 | "force_ssl": false 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /fixtures/smartcollection.json: -------------------------------------------------------------------------------- 1 | {"smart_collection":{"id":30497275952,"handle":"macbooks","title":"Macbooks","updated_at":"2018-02-06T02:20:25-05:00","body_html":"Macbook Body","published_at":"2018-02-06T02:20:25-05:00","sort_order":"best-selling","template_suffix":null,"published_scope":"web", "disjunctive": true, "rules": [{"column":"title","relation":"contains","condition":"mac"}]}} 2 | -------------------------------------------------------------------------------- /fixtures/transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "transaction": { 3 | "id": 389404469, 4 | "order_id": 450789469, 5 | "amount": "409.94", 6 | "kind": "authorization", 7 | "gateway": "bogus", 8 | "status": "success", 9 | "message": "Bogus Gateway: Forced success", 10 | "created_at": "2017-07-24T19:09:43-00:00", 11 | "test": true, 12 | "authorization": "authorization-key", 13 | "currency": "USD", 14 | "location_id": null, 15 | "user_id": null, 16 | "parent_id": null, 17 | "device_id": null, 18 | "receipt": { 19 | "testcase": true, 20 | "authorization": "123456" 21 | }, 22 | "error_code": null, 23 | "source_name": "web", 24 | "payment_details": { 25 | "credit_card_bin": null, 26 | "avs_result_code": null, 27 | "cvv_result_code": null, 28 | "credit_card_number": "•••• •••• •••• 4242", 29 | "credit_card_company": "Visa" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /fixtures/transactions.json: -------------------------------------------------------------------------------- 1 | { 2 | "transactions": [ 3 | { 4 | "id": 389404469, 5 | "order_id": 450789469, 6 | "amount": "409.94", 7 | "kind": "authorization", 8 | "gateway": "bogus", 9 | "status": "success", 10 | "message": "Bogus Gateway: Forced success", 11 | "created_at": "2017-07-24T19:09:43+00:00", 12 | "test": true, 13 | "authorization": "authorization-key", 14 | "currency": "USD", 15 | "location_id": null, 16 | "user_id": null, 17 | "parent_id": null, 18 | "device_id": null, 19 | "receipt": { 20 | "testcase": true, 21 | "authorization": "123456" 22 | }, 23 | "error_code": null, 24 | "source_name": "web", 25 | "payment_details": { 26 | "credit_card_bin": null, 27 | "avs_result_code": null, 28 | "cvv_result_code": null, 29 | "credit_card_number": "•••• •••• •••• 4242", 30 | "credit_card_company": "Visa" 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /fixtures/variant.json: -------------------------------------------------------------------------------- 1 | { 2 | "variant": { 3 | "id": 1, 4 | "product_id": 1, 5 | "title": "Yellow", 6 | "price": "1.00", 7 | "sku": "", 8 | "position": 2, 9 | "inventory_policy": "deny", 10 | "compare_at_price": null, 11 | "fulfillment_service": "manual", 12 | "inventory_management": null, 13 | "option1": "Yellow", 14 | "option2": null, 15 | "option3": null, 16 | "created_at": "2017-10-18T12:22:36-04:00", 17 | "updated_at": "2017-10-18T12:22:36-04:00", 18 | "taxable": true, 19 | "barcode": null, 20 | "grams": 0, 21 | "image_id": null, 22 | "inventory_quantity": 1, 23 | "weight": 0, 24 | "weight_unit": "lb", 25 | "old_inventory_quantity": 1, 26 | "requires_shipping": true 27 | } 28 | } -------------------------------------------------------------------------------- /fixtures/webhook.json: -------------------------------------------------------------------------------- 1 | { 2 | "webhook": { 3 | "id": 4759306, 4 | "address": "http:\/\/apple.com", 5 | "topic": "orders\/create", 6 | "created_at": "2016-06-01T14:10:44-00:00", 7 | "updated_at": "2016-06-01T14:10:44-00:00", 8 | "format": "json", 9 | "fields": [ 10 | "id", "updated_at" 11 | ], 12 | "metafield_namespaces": [ 13 | "google", "inventory" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /fixtures/webhooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "webhooks": [ 3 | { 4 | "id": 4759306, 5 | "address": "http:\/\/apple.com", 6 | "topic": "orders\/create", 7 | "created_at": "2016-06-01T14:10:44-00:00", 8 | "updated_at": "2016-06-01T14:10:44-00:00", 9 | "format": "json", 10 | "fields": [ 11 | "id", "updated_at" 12 | ], 13 | "metafield_namespaces": [ 14 | "google", "inventory" 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /fulfillment.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // FulfillmentService is an interface for interfacing with the fulfillment endpoints 9 | // of the Shopify API. 10 | // https://help.shopify.com/api/reference/fulfillment 11 | type FulfillmentService interface { 12 | List(interface{}) ([]Fulfillment, error) 13 | Count(interface{}) (int, error) 14 | Get(int, interface{}) (*Fulfillment, error) 15 | Create(Fulfillment) (*Fulfillment, error) 16 | Update(Fulfillment) (*Fulfillment, error) 17 | Complete(int) (*Fulfillment, error) 18 | Transition(int) (*Fulfillment, error) 19 | Cancel(int) (*Fulfillment, error) 20 | } 21 | 22 | // FulfillmentsService is an interface for other Shopify resources 23 | // to interface with the fulfillment endpoints of the Shopify API. 24 | // https://help.shopify.com/api/reference/fulfillment 25 | type FulfillmentsService interface { 26 | ListFulfillments(int, interface{}) ([]Fulfillment, error) 27 | CountFulfillments(int, interface{}) (int, error) 28 | GetFulfillment(int, int, interface{}) (*Fulfillment, error) 29 | CreateFulfillment(int, Fulfillment) (*Fulfillment, error) 30 | UpdateFulfillment(int, Fulfillment) (*Fulfillment, error) 31 | CompleteFulfillment(int, int) (*Fulfillment, error) 32 | TransitionFulfillment(int, int) (*Fulfillment, error) 33 | CancelFulfillment(int, int) (*Fulfillment, error) 34 | } 35 | 36 | // FulfillmentServiceOp handles communication with the fulfillment 37 | // related methods of the Shopify API. 38 | type FulfillmentServiceOp struct { 39 | client *Client 40 | resource string 41 | resourceID int 42 | } 43 | 44 | // Fulfillment represents a Shopify fulfillment. 45 | type Fulfillment struct { 46 | ID int `json:"id,omitempty"` 47 | OrderID int `json:"order_id,omitempty"` 48 | LocationID int `json:"location_id,omitempty"` 49 | Status string `json:"status,omitempty"` 50 | CreatedAt *time.Time `json:"created_at,omitempty"` 51 | Service string `json:"service,omitempty"` 52 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 53 | TrackingCompany string `json:"tracking_company,omitempty"` 54 | ShipmentStatus string `json:"shipment_status,omitempty"` 55 | TrackingNumber string `json:"tracking_number,omitempty"` 56 | TrackingNumbers []string `json:"tracking_numbers,omitempty"` 57 | TrackingUrl string `json:"tracking_url,omitempty"` 58 | TrackingUrls []string `json:"tracking_urls,omitempty"` 59 | Receipt Receipt `json:"receipt,omitempty"` 60 | LineItems []LineItem `json:"line_items,omitempty"` 61 | NotifyCustomer bool `json:"notify_customer,omitempty"` 62 | } 63 | 64 | // Receipt represents a Shopify receipt. 65 | type Receipt struct { 66 | TestCase bool `json:"testcase,omitempty"` 67 | Authorization string `json:"authorization,omitempty"` 68 | } 69 | 70 | // FulfillmentResource represents the result from the fulfillments/X.json endpoint 71 | type FulfillmentResource struct { 72 | Fulfillment *Fulfillment `json:"fulfillment"` 73 | } 74 | 75 | // FulfillmentsResource represents the result from the fullfilments.json endpoint 76 | type FulfillmentsResource struct { 77 | Fulfillments []Fulfillment `json:"fulfillments"` 78 | } 79 | 80 | // List fulfillments 81 | func (s *FulfillmentServiceOp) List(options interface{}) ([]Fulfillment, error) { 82 | prefix := FulfillmentPathPrefix(s.resource, s.resourceID) 83 | path := fmt.Sprintf("%s.json", prefix) 84 | resource := new(FulfillmentsResource) 85 | err := s.client.Get(path, resource, options) 86 | return resource.Fulfillments, err 87 | } 88 | 89 | // Count fulfillments 90 | func (s *FulfillmentServiceOp) Count(options interface{}) (int, error) { 91 | prefix := FulfillmentPathPrefix(s.resource, s.resourceID) 92 | path := fmt.Sprintf("%s/count.json", prefix) 93 | return s.client.Count(path, options) 94 | } 95 | 96 | // Get individual fulfillment 97 | func (s *FulfillmentServiceOp) Get(fulfillmentID int, options interface{}) (*Fulfillment, error) { 98 | prefix := FulfillmentPathPrefix(s.resource, s.resourceID) 99 | path := fmt.Sprintf("%s/%d.json", prefix, fulfillmentID) 100 | resource := new(FulfillmentResource) 101 | err := s.client.Get(path, resource, options) 102 | return resource.Fulfillment, err 103 | } 104 | 105 | // Create a new fulfillment 106 | func (s *FulfillmentServiceOp) Create(fulfillment Fulfillment) (*Fulfillment, error) { 107 | prefix := FulfillmentPathPrefix(s.resource, s.resourceID) 108 | path := fmt.Sprintf("%s.json", prefix) 109 | wrappedData := FulfillmentResource{Fulfillment: &fulfillment} 110 | resource := new(FulfillmentResource) 111 | err := s.client.Post(path, wrappedData, resource) 112 | return resource.Fulfillment, err 113 | } 114 | 115 | // Update an existing fulfillment 116 | func (s *FulfillmentServiceOp) Update(fulfillment Fulfillment) (*Fulfillment, error) { 117 | prefix := FulfillmentPathPrefix(s.resource, s.resourceID) 118 | path := fmt.Sprintf("%s/%d.json", prefix, fulfillment.ID) 119 | wrappedData := FulfillmentResource{Fulfillment: &fulfillment} 120 | resource := new(FulfillmentResource) 121 | err := s.client.Put(path, wrappedData, resource) 122 | return resource.Fulfillment, err 123 | } 124 | 125 | // Complete an existing fulfillment 126 | func (s *FulfillmentServiceOp) Complete(fulfillmentID int) (*Fulfillment, error) { 127 | prefix := FulfillmentPathPrefix(s.resource, s.resourceID) 128 | path := fmt.Sprintf("%s/%d/complete.json", prefix, fulfillmentID) 129 | resource := new(FulfillmentResource) 130 | err := s.client.Post(path, nil, resource) 131 | return resource.Fulfillment, err 132 | } 133 | 134 | // Transition an existing fulfillment 135 | func (s *FulfillmentServiceOp) Transition(fulfillmentID int) (*Fulfillment, error) { 136 | prefix := FulfillmentPathPrefix(s.resource, s.resourceID) 137 | path := fmt.Sprintf("%s/%d/open.json", prefix, fulfillmentID) 138 | resource := new(FulfillmentResource) 139 | err := s.client.Post(path, nil, resource) 140 | return resource.Fulfillment, err 141 | } 142 | 143 | // Cancel an existing fulfillment 144 | func (s *FulfillmentServiceOp) Cancel(fulfillmentID int) (*Fulfillment, error) { 145 | prefix := FulfillmentPathPrefix(s.resource, s.resourceID) 146 | path := fmt.Sprintf("%s/%d/cancel.json", prefix, fulfillmentID) 147 | resource := new(FulfillmentResource) 148 | err := s.client.Post(path, nil, resource) 149 | return resource.Fulfillment, err 150 | } 151 | -------------------------------------------------------------------------------- /fulfillment_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | httpmock "gopkg.in/jarcoal/httpmock.v1" 9 | ) 10 | 11 | func FulfillmentTests(t *testing.T, fulfillment Fulfillment) { 12 | // Check that ID is assigned to the returned fulfillment 13 | expectedInt := 1022782888 14 | if fulfillment.ID != expectedInt { 15 | t.Errorf("Fulfillment.ID returned %+v, expected %+v", fulfillment.ID, expectedInt) 16 | } 17 | } 18 | 19 | func TestFulfillmentList(t *testing.T) { 20 | setup() 21 | defer teardown() 22 | 23 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/123/fulfillments.json", 24 | httpmock.NewStringResponder(200, `{"fulfillments": [{"id":1},{"id":2}]}`)) 25 | 26 | fulfillmentService := &FulfillmentServiceOp{client: client, resource: ordersResourceName, resourceID: 123} 27 | 28 | fulfillments, err := fulfillmentService.List(nil) 29 | if err != nil { 30 | t.Errorf("Fulfillment.List returned error: %v", err) 31 | } 32 | 33 | expected := []Fulfillment{{ID: 1}, {ID: 2}} 34 | if !reflect.DeepEqual(fulfillments, expected) { 35 | t.Errorf("Fulfillment.List returned %+v, expected %+v", fulfillments, expected) 36 | } 37 | } 38 | 39 | func TestFulfillmentCount(t *testing.T) { 40 | setup() 41 | defer teardown() 42 | 43 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/count.json", 44 | httpmock.NewStringResponder(200, `{"count": 3}`)) 45 | 46 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/count.json?created_at_min=2016-01-01T00%3A00%3A00Z", 47 | httpmock.NewStringResponder(200, `{"count": 2}`)) 48 | 49 | fulfillmentService := &FulfillmentServiceOp{client: client, resource: ordersResourceName, resourceID: 123} 50 | 51 | cnt, err := fulfillmentService.Count(nil) 52 | if err != nil { 53 | t.Errorf("Fulfillment.Count returned error: %v", err) 54 | } 55 | 56 | expected := 3 57 | if cnt != expected { 58 | t.Errorf("Fulfillment.Count returned %d, expected %d", cnt, expected) 59 | } 60 | 61 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 62 | cnt, err = fulfillmentService.Count(CountOptions{CreatedAtMin: date}) 63 | if err != nil { 64 | t.Errorf("Fulfillment.Count returned error: %v", err) 65 | } 66 | 67 | expected = 2 68 | if cnt != expected { 69 | t.Errorf("Fulfillment.Count returned %d, expected %d", cnt, expected) 70 | } 71 | } 72 | 73 | func TestFulfillmentGet(t *testing.T) { 74 | setup() 75 | defer teardown() 76 | 77 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1.json", 78 | httpmock.NewStringResponder(200, `{"fulfillment": {"id":1}}`)) 79 | 80 | fulfillmentService := &FulfillmentServiceOp{client: client, resource: ordersResourceName, resourceID: 123} 81 | 82 | fulfillment, err := fulfillmentService.Get(1, nil) 83 | if err != nil { 84 | t.Errorf("Fulfillment.Get returned error: %v", err) 85 | } 86 | 87 | expected := &Fulfillment{ID: 1} 88 | if !reflect.DeepEqual(fulfillment, expected) { 89 | t.Errorf("Fulfillment.Get returned %+v, expected %+v", fulfillment, expected) 90 | } 91 | } 92 | 93 | func TestFulfillmentCreate(t *testing.T) { 94 | setup() 95 | defer teardown() 96 | 97 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/orders/123/fulfillments.json", 98 | httpmock.NewBytesResponder(200, loadFixture("fulfillment.json"))) 99 | 100 | fulfillmentService := &FulfillmentServiceOp{client: client, resource: ordersResourceName, resourceID: 123} 101 | 102 | fulfillment := Fulfillment{ 103 | LocationID: 905684977, 104 | TrackingNumber: "123456789", 105 | TrackingUrls: []string{ 106 | "https://shipping.xyz/track.php?num=123456789", 107 | "https://anothershipper.corp/track.php?code=abc", 108 | }, 109 | NotifyCustomer: true, 110 | } 111 | 112 | returnedFulfillment, err := fulfillmentService.Create(fulfillment) 113 | if err != nil { 114 | t.Errorf("Fulfillment.Create returned error: %v", err) 115 | } 116 | 117 | FulfillmentTests(t, *returnedFulfillment) 118 | } 119 | 120 | func TestFulfillmentUpdate(t *testing.T) { 121 | setup() 122 | defer teardown() 123 | 124 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1022782888.json", 125 | httpmock.NewBytesResponder(200, loadFixture("fulfillment.json"))) 126 | 127 | fulfillmentService := &FulfillmentServiceOp{client: client, resource: ordersResourceName, resourceID: 123} 128 | 129 | fulfillment := Fulfillment{ 130 | ID: 1022782888, 131 | TrackingNumber: "987654321", 132 | } 133 | 134 | returnedFulfillment, err := fulfillmentService.Update(fulfillment) 135 | if err != nil { 136 | t.Errorf("Fulfillment.Update returned error: %v", err) 137 | } 138 | 139 | FulfillmentTests(t, *returnedFulfillment) 140 | } 141 | 142 | func TestFulfillmentComplete(t *testing.T) { 143 | setup() 144 | defer teardown() 145 | 146 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1/complete.json", 147 | httpmock.NewBytesResponder(200, loadFixture("fulfillment.json"))) 148 | 149 | fulfillmentService := &FulfillmentServiceOp{client: client, resource: ordersResourceName, resourceID: 123} 150 | 151 | returnedFulfillment, err := fulfillmentService.Complete(1) 152 | if err != nil { 153 | t.Errorf("Fulfillment.Complete returned error: %v", err) 154 | } 155 | 156 | FulfillmentTests(t, *returnedFulfillment) 157 | } 158 | 159 | func TestFulfillmentTransition(t *testing.T) { 160 | setup() 161 | defer teardown() 162 | 163 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1/open.json", 164 | httpmock.NewBytesResponder(200, loadFixture("fulfillment.json"))) 165 | 166 | fulfillmentService := &FulfillmentServiceOp{client: client, resource: ordersResourceName, resourceID: 123} 167 | 168 | returnedFulfillment, err := fulfillmentService.Transition(1) 169 | if err != nil { 170 | t.Errorf("Fulfillment.Transition returned error: %v", err) 171 | } 172 | 173 | FulfillmentTests(t, *returnedFulfillment) 174 | } 175 | 176 | func TestFulfillmentCancel(t *testing.T) { 177 | setup() 178 | defer teardown() 179 | 180 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/orders/123/fulfillments/1/cancel.json", 181 | httpmock.NewBytesResponder(200, loadFixture("fulfillment.json"))) 182 | 183 | fulfillmentService := &FulfillmentServiceOp{client: client, resource: ordersResourceName, resourceID: 123} 184 | 185 | returnedFulfillment, err := fulfillmentService.Cancel(1) 186 | if err != nil { 187 | t.Errorf("Fulfillment.Cancel returned error: %v", err) 188 | } 189 | 190 | FulfillmentTests(t, *returnedFulfillment) 191 | } 192 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // ImageService is an interface for interacting with the image endpoints 9 | // of the Shopify API. 10 | // See https://help.shopify.com/api/reference/product_image 11 | type ImageService interface { 12 | List(int, interface{}) ([]Image, error) 13 | Count(int, interface{}) (int, error) 14 | Get(int, int, interface{}) (*Image, error) 15 | Create(int, Image) (*Image, error) 16 | Update(int, Image) (*Image, error) 17 | Delete(int, int) error 18 | } 19 | 20 | // ImageServiceOp handles communication with the image related methods of 21 | // the Shopify API. 22 | type ImageServiceOp struct { 23 | client *Client 24 | } 25 | 26 | // Image represents a Shopify product's image. 27 | type Image struct { 28 | ID int `json:"id"` 29 | ProductID int `json:"product_id"` 30 | Position int `json:"position"` 31 | CreatedAt *time.Time `json:"created_at"` 32 | UpdatedAt *time.Time `json:"updated_at"` 33 | Width int `json:"width"` 34 | Height int `json:"height"` 35 | Src string `json:"src,omitempty"` 36 | Attachment string `json:"attachment,omitempty"` 37 | Filename string `json:"filename,omitempty"` 38 | VariantIds []int `json:"variant_ids"` 39 | } 40 | 41 | // ImageResource represents the result form the products/X/images/Y.json endpoint 42 | type ImageResource struct { 43 | Image *Image `json:"image"` 44 | } 45 | 46 | // ImagesResource represents the result from the products/X/images.json endpoint 47 | type ImagesResource struct { 48 | Images []Image `json:"images"` 49 | } 50 | 51 | // List images 52 | func (s *ImageServiceOp) List(productID int, options interface{}) ([]Image, error) { 53 | path := fmt.Sprintf("%s/%d/images.json", productsBasePath, productID) 54 | resource := new(ImagesResource) 55 | err := s.client.Get(path, resource, options) 56 | return resource.Images, err 57 | } 58 | 59 | // Count images 60 | func (s *ImageServiceOp) Count(productID int, options interface{}) (int, error) { 61 | path := fmt.Sprintf("%s/%d/images/count.json", productsBasePath, productID) 62 | return s.client.Count(path, options) 63 | } 64 | 65 | // Get individual image 66 | func (s *ImageServiceOp) Get(productID int, imageID int, options interface{}) (*Image, error) { 67 | path := fmt.Sprintf("%s/%d/images/%d.json", productsBasePath, productID, imageID) 68 | resource := new(ImageResource) 69 | err := s.client.Get(path, resource, options) 70 | return resource.Image, err 71 | } 72 | 73 | // Create a new image 74 | // 75 | // There are 2 methods of creating an image in Shopify: 76 | // 1. Src 77 | // 2. Filename and Attachment 78 | // 79 | // If both Image.Filename and Image.Attachment are supplied, 80 | // then Image.Src is not needed. And vice versa. 81 | // 82 | // If both Image.Attachment and Image.Src are provided, 83 | // Shopify will take the attachment. 84 | // 85 | // Shopify will accept Image.Attachment without Image.Filename. 86 | func (s *ImageServiceOp) Create(productID int, image Image) (*Image, error) { 87 | path := fmt.Sprintf("%s/%d/images.json", productsBasePath, productID) 88 | wrappedData := ImageResource{Image: &image} 89 | resource := new(ImageResource) 90 | err := s.client.Post(path, wrappedData, resource) 91 | return resource.Image, err 92 | } 93 | 94 | // Update an existing image 95 | func (s *ImageServiceOp) Update(productID int, image Image) (*Image, error) { 96 | path := fmt.Sprintf("%s/%d/images/%d.json", productsBasePath, productID, image.ID) 97 | wrappedData := ImageResource{Image: &image} 98 | resource := new(ImageResource) 99 | err := s.client.Put(path, wrappedData, resource) 100 | return resource.Image, err 101 | } 102 | 103 | // Delete an existing image 104 | func (s *ImageServiceOp) Delete(productID int, imageID int) error { 105 | return s.client.Delete(fmt.Sprintf("%s/%d/images/%d.json", productsBasePath, productID, imageID)) 106 | } 107 | -------------------------------------------------------------------------------- /image_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "gopkg.in/jarcoal/httpmock.v1" 8 | ) 9 | 10 | func imageTests(t *testing.T, image Image) { 11 | // Check that ID is set 12 | expectedImageID := 1 13 | if image.ID != expectedImageID { 14 | t.Errorf("Image.ID returned %+v, expected %+v", image.ID, expectedImageID) 15 | } 16 | 17 | // Check that product_id is set 18 | expectedProductID := 1 19 | if image.ProductID != expectedProductID { 20 | t.Errorf("Image.ProductID returned %+v, expected %+v", image.ProductID, expectedProductID) 21 | } 22 | 23 | // Check that position is set 24 | expectedPosition := 1 25 | if image.Position != expectedPosition { 26 | t.Errorf("Image.Position returned %+v, expected %+v", image.Position, expectedPosition) 27 | } 28 | 29 | // Check that width is set 30 | expectedWidth := 123 31 | if image.Width != expectedWidth { 32 | t.Errorf("Image.Width returned %+v, expected %+v", image.Width, expectedWidth) 33 | } 34 | 35 | // Check that height is set 36 | expectedHeight := 456 37 | if image.Height != expectedHeight { 38 | t.Errorf("Image.Height returned %+v, expected %+v", image.Height, expectedHeight) 39 | } 40 | 41 | // Check that src is set 42 | expectedSrc := "https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1500937783" 43 | if image.Src != expectedSrc { 44 | t.Errorf("Image.Src returned %+v, expected %+v", image.Src, expectedSrc) 45 | } 46 | 47 | // Check that variant ids are set 48 | expectedVariantIds := make([]int, 2) 49 | expectedVariantIds[0] = 808950810 50 | expectedVariantIds[1] = 808950811 51 | 52 | if image.VariantIds[0] != expectedVariantIds[0] { 53 | t.Errorf("Image.VariantIds[0] returned %+v, expected %+v", image.VariantIds[0], expectedVariantIds[0]) 54 | } 55 | if image.VariantIds[1] != expectedVariantIds[1] { 56 | t.Errorf("Image.VariantIds[0] returned %+v, expected %+v", image.VariantIds[1], expectedVariantIds[1]) 57 | } 58 | 59 | // Check that CreatedAt date is set 60 | expectedCreatedAt := time.Date(2017, time.July, 24, 19, 9, 43, 0, time.UTC) 61 | if !expectedCreatedAt.Equal(*image.CreatedAt) { 62 | t.Errorf("Image.CreatedAt returned %+v, expected %+v", image.CreatedAt, expectedCreatedAt) 63 | } 64 | 65 | // Check that UpdatedAt date is set 66 | expectedUpdatedAt := time.Date(2017, time.July, 24, 19, 9, 43, 0, time.UTC) 67 | if !expectedUpdatedAt.Equal(*image.UpdatedAt) { 68 | t.Errorf("Image.UpdatedAt returned %+v, expected %+v", image.UpdatedAt, expectedUpdatedAt) 69 | } 70 | } 71 | 72 | func TestImageList(t *testing.T) { 73 | setup() 74 | defer teardown() 75 | 76 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/images.json", 77 | httpmock.NewBytesResponder(200, loadFixture("images.json"))) 78 | 79 | images, err := client.Image.List(1, nil) 80 | if err != nil { 81 | t.Errorf("Images.List returned error: %v", err) 82 | } 83 | 84 | // Check that images were parsed 85 | if len(images) != 2 { 86 | t.Errorf("Image.List got %v images, expected 2", len(images)) 87 | } 88 | 89 | imageTests(t, images[0]) 90 | } 91 | 92 | func TestImageCount(t *testing.T) { 93 | setup() 94 | defer teardown() 95 | 96 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/images/count.json", 97 | httpmock.NewStringResponder(200, `{"count": 2}`)) 98 | 99 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/images/count.json?created_at_min=2016-01-01T00%3A00%3A00Z", 100 | httpmock.NewStringResponder(200, `{"count": 1}`)) 101 | 102 | cnt, err := client.Image.Count(1, nil) 103 | if err != nil { 104 | t.Errorf("Image.Count returned error: %v", err) 105 | } 106 | 107 | expected := 2 108 | if cnt != expected { 109 | t.Errorf("Image.Count returned %d, expected %d", cnt, expected) 110 | } 111 | 112 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 113 | cnt, err = client.Image.Count(1, CountOptions{CreatedAtMin: date}) 114 | if err != nil { 115 | t.Errorf("Image.Count returned %d, expected %d", cnt, expected) 116 | } 117 | 118 | expected = 1 119 | if cnt != expected { 120 | t.Errorf("Image.Count returned %d, expected %d", cnt, expected) 121 | } 122 | } 123 | 124 | func TestImageGet(t *testing.T) { 125 | setup() 126 | defer teardown() 127 | 128 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/images/1.json", 129 | httpmock.NewBytesResponder(200, loadFixture("image.json"))) 130 | 131 | image, err := client.Image.Get(1, 1, nil) 132 | if err != nil { 133 | t.Errorf("Image.Get returned error: %v", err) 134 | } 135 | 136 | imageTests(t, *image) 137 | } 138 | 139 | func TestImageCreate(t *testing.T) { 140 | setup() 141 | defer teardown() 142 | 143 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/products/1/images.json", 144 | httpmock.NewBytesResponder(200, loadFixture("image.json"))) 145 | 146 | variantIds := make([]int, 2) 147 | variantIds[0] = 808950810 148 | variantIds[1] = 808950811 149 | 150 | image := Image{ 151 | Src: "https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1500937783", 152 | VariantIds: variantIds, 153 | } 154 | returnedImage, err := client.Image.Create(1, image) 155 | if err != nil { 156 | t.Errorf("Image.Create returned error %v", err) 157 | } 158 | 159 | imageTests(t, *returnedImage) 160 | } 161 | 162 | func TestImageUpdate(t *testing.T) { 163 | setup() 164 | defer teardown() 165 | 166 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/products/1/images/1.json", 167 | httpmock.NewBytesResponder(200, loadFixture("image.json"))) 168 | 169 | // Take an existing image 170 | variantIds := make([]int, 2) 171 | variantIds[0] = 808950810 172 | variantIds[1] = 457924702 173 | existingImage := Image{ 174 | ID: 1, 175 | VariantIds: variantIds, 176 | } 177 | // And update it 178 | existingImage.VariantIds[1] = 808950811 179 | returnedImage, err := client.Image.Update(1, existingImage) 180 | if err != nil { 181 | t.Errorf("Image.Update returned error %v", err) 182 | } 183 | 184 | imageTests(t, *returnedImage) 185 | } 186 | 187 | func TestImageDelete(t *testing.T) { 188 | setup() 189 | defer teardown() 190 | 191 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/products/1/images/1.json", 192 | httpmock.NewStringResponder(200, "{}")) 193 | 194 | err := client.Image.Delete(1, 1) 195 | if err != nil { 196 | t.Errorf("Image.Delete returned error: %v", err) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /metafield.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // MetafieldService is an interface for interfacing with the metafield endpoints 9 | // of the Shopify API. 10 | // https://help.shopify.com/api/reference/metafield 11 | type MetafieldService interface { 12 | List(interface{}) ([]Metafield, error) 13 | Count(interface{}) (int, error) 14 | Get(int, interface{}) (*Metafield, error) 15 | Create(Metafield) (*Metafield, error) 16 | Update(Metafield) (*Metafield, error) 17 | Delete(int) error 18 | } 19 | 20 | // MetafieldsService is an interface for other Shopify resources 21 | // to interface with the metafield endpoints of the Shopify API. 22 | // https://help.shopify.com/api/reference/metafield 23 | type MetafieldsService interface { 24 | ListMetafields(int, interface{}) ([]Metafield, error) 25 | CountMetafields(int, interface{}) (int, error) 26 | GetMetafield(int, int, interface{}) (*Metafield, error) 27 | CreateMetafield(int, Metafield) (*Metafield, error) 28 | UpdateMetafield(int, Metafield) (*Metafield, error) 29 | DeleteMetafield(int, int) error 30 | } 31 | 32 | // MetafieldServiceOp handles communication with the metafield 33 | // related methods of the Shopify API. 34 | type MetafieldServiceOp struct { 35 | client *Client 36 | resource string 37 | resourceID int 38 | } 39 | 40 | // Metafield represents a Shopify metafield. 41 | type Metafield struct { 42 | ID int `json:"id,omitempty"` 43 | Key string `json:"key,omitempty"` 44 | Value interface{} `json:"value,omitempty"` 45 | ValueType string `json:"value_type,omitempty"` 46 | Namespace string `json:"namespace,omitempty"` 47 | Description string `json:"description,omitempty"` 48 | OwnerId int `json:"owner_id,omitempty"` 49 | CreatedAt *time.Time `json:"created_at,omitempty"` 50 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 51 | OwnerResource string `json:"owner_resource,omitempty"` 52 | } 53 | 54 | // MetafieldResource represents the result from the metafields/X.json endpoint 55 | type MetafieldResource struct { 56 | Metafield *Metafield `json:"metafield"` 57 | } 58 | 59 | // MetafieldsResource represents the result from the metafields.json endpoint 60 | type MetafieldsResource struct { 61 | Metafields []Metafield `json:"metafields"` 62 | } 63 | 64 | // List metafields 65 | func (s *MetafieldServiceOp) List(options interface{}) ([]Metafield, error) { 66 | prefix := MetafieldPathPrefix(s.resource, s.resourceID) 67 | path := fmt.Sprintf("%s.json", prefix) 68 | resource := new(MetafieldsResource) 69 | err := s.client.Get(path, resource, options) 70 | return resource.Metafields, err 71 | } 72 | 73 | // Count metafields 74 | func (s *MetafieldServiceOp) Count(options interface{}) (int, error) { 75 | prefix := MetafieldPathPrefix(s.resource, s.resourceID) 76 | path := fmt.Sprintf("%s/count.json", prefix) 77 | return s.client.Count(path, options) 78 | } 79 | 80 | // Get individual metafield 81 | func (s *MetafieldServiceOp) Get(metafieldID int, options interface{}) (*Metafield, error) { 82 | prefix := MetafieldPathPrefix(s.resource, s.resourceID) 83 | path := fmt.Sprintf("%s/%d.json", prefix, metafieldID) 84 | resource := new(MetafieldResource) 85 | err := s.client.Get(path, resource, options) 86 | return resource.Metafield, err 87 | } 88 | 89 | // Create a new metafield 90 | func (s *MetafieldServiceOp) Create(metafield Metafield) (*Metafield, error) { 91 | prefix := MetafieldPathPrefix(s.resource, s.resourceID) 92 | path := fmt.Sprintf("%s.json", prefix) 93 | wrappedData := MetafieldResource{Metafield: &metafield} 94 | resource := new(MetafieldResource) 95 | err := s.client.Post(path, wrappedData, resource) 96 | return resource.Metafield, err 97 | } 98 | 99 | // Update an existing metafield 100 | func (s *MetafieldServiceOp) Update(metafield Metafield) (*Metafield, error) { 101 | prefix := MetafieldPathPrefix(s.resource, s.resourceID) 102 | path := fmt.Sprintf("%s/%d.json", prefix, metafield.ID) 103 | wrappedData := MetafieldResource{Metafield: &metafield} 104 | resource := new(MetafieldResource) 105 | err := s.client.Put(path, wrappedData, resource) 106 | return resource.Metafield, err 107 | } 108 | 109 | // Delete an existing metafield 110 | func (s *MetafieldServiceOp) Delete(metafieldID int) error { 111 | prefix := MetafieldPathPrefix(s.resource, s.resourceID) 112 | return s.client.Delete(fmt.Sprintf("%s/%d.json", prefix, metafieldID)) 113 | } 114 | -------------------------------------------------------------------------------- /metafield_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | httpmock "gopkg.in/jarcoal/httpmock.v1" 9 | ) 10 | 11 | func MetafieldTests(t *testing.T, metafield Metafield) { 12 | // Check that ID is assigned to the returned metafield 13 | expectedInt := 721389482 14 | if metafield.ID != expectedInt { 15 | t.Errorf("Metafield.ID returned %+v, expected %+v", metafield.ID, expectedInt) 16 | } 17 | } 18 | 19 | func TestMetafieldList(t *testing.T) { 20 | setup() 21 | defer teardown() 22 | 23 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/metafields.json", 24 | httpmock.NewStringResponder(200, `{"metafields": [{"id":1},{"id":2}]}`)) 25 | 26 | metafields, err := client.Metafield.List(nil) 27 | if err != nil { 28 | t.Errorf("Metafield.List returned error: %v", err) 29 | } 30 | 31 | expected := []Metafield{{ID: 1}, {ID: 2}} 32 | if !reflect.DeepEqual(metafields, expected) { 33 | t.Errorf("Metafield.List returned %+v, expected %+v", metafields, expected) 34 | } 35 | } 36 | 37 | func TestMetafieldCount(t *testing.T) { 38 | setup() 39 | defer teardown() 40 | 41 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/metafields/count.json", 42 | httpmock.NewStringResponder(200, `{"count": 3}`)) 43 | 44 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/metafields/count.json?created_at_min=2016-01-01T00%3A00%3A00Z", 45 | httpmock.NewStringResponder(200, `{"count": 2}`)) 46 | 47 | cnt, err := client.Metafield.Count(nil) 48 | if err != nil { 49 | t.Errorf("Metafield.Count returned error: %v", err) 50 | } 51 | 52 | expected := 3 53 | if cnt != expected { 54 | t.Errorf("Metafield.Count returned %d, expected %d", cnt, expected) 55 | } 56 | 57 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 58 | cnt, err = client.Metafield.Count(CountOptions{CreatedAtMin: date}) 59 | if err != nil { 60 | t.Errorf("Metafield.Count returned error: %v", err) 61 | } 62 | 63 | expected = 2 64 | if cnt != expected { 65 | t.Errorf("Metafield.Count returned %d, expected %d", cnt, expected) 66 | } 67 | } 68 | 69 | func TestMetafieldGet(t *testing.T) { 70 | setup() 71 | defer teardown() 72 | 73 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/metafields/1.json", 74 | httpmock.NewStringResponder(200, `{"metafield": {"id":1}}`)) 75 | 76 | metafield, err := client.Metafield.Get(1, nil) 77 | if err != nil { 78 | t.Errorf("Metafield.Get returned error: %v", err) 79 | } 80 | 81 | expected := &Metafield{ID: 1} 82 | if !reflect.DeepEqual(metafield, expected) { 83 | t.Errorf("Metafield.Get returned %+v, expected %+v", metafield, expected) 84 | } 85 | } 86 | 87 | func TestMetafieldCreate(t *testing.T) { 88 | setup() 89 | defer teardown() 90 | 91 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/metafields.json", 92 | httpmock.NewBytesResponder(200, loadFixture("metafield.json"))) 93 | 94 | metafield := Metafield{ 95 | Namespace: "inventory", 96 | Key: "warehouse", 97 | Value: "25", 98 | ValueType: "integer", 99 | } 100 | 101 | returnedMetafield, err := client.Metafield.Create(metafield) 102 | if err != nil { 103 | t.Errorf("Metafield.Create returned error: %v", err) 104 | } 105 | 106 | MetafieldTests(t, *returnedMetafield) 107 | } 108 | 109 | func TestMetafieldUpdate(t *testing.T) { 110 | setup() 111 | defer teardown() 112 | 113 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/metafields/1.json", 114 | httpmock.NewBytesResponder(200, loadFixture("metafield.json"))) 115 | 116 | metafield := Metafield{ 117 | ID: 1, 118 | Value: "something new", 119 | ValueType: "string", 120 | } 121 | 122 | returnedMetafield, err := client.Metafield.Update(metafield) 123 | if err != nil { 124 | t.Errorf("Metafield.Update returned error: %v", err) 125 | } 126 | 127 | MetafieldTests(t, *returnedMetafield) 128 | } 129 | 130 | func TestMetafieldDelete(t *testing.T) { 131 | setup() 132 | defer teardown() 133 | 134 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/metafields/1.json", 135 | httpmock.NewStringResponder(200, "{}")) 136 | 137 | err := client.Metafield.Delete(1) 138 | if err != nil { 139 | t.Errorf("Metafield.Delete returned error: %v", err) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /oauth.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | ) 13 | 14 | const shopifyChecksumHeader = "X-Shopify-Hmac-Sha256" 15 | 16 | // Returns a Shopify oauth authorization url for the given shopname and state. 17 | // 18 | // State is a unique value that can be used to check the authenticity during a 19 | // callback from Shopify. 20 | func (app App) AuthorizeUrl(shopName string, state string) string { 21 | shopUrl, _ := url.Parse(ShopBaseUrl(shopName)) 22 | shopUrl.Path = "/admin/oauth/authorize" 23 | query := shopUrl.Query() 24 | query.Set("client_id", app.ApiKey) 25 | query.Set("redirect_uri", app.RedirectUrl) 26 | query.Set("scope", app.Scope) 27 | query.Set("state", state) 28 | shopUrl.RawQuery = query.Encode() 29 | return shopUrl.String() 30 | } 31 | 32 | func (app App) GetAccessToken(shopName string, code string) (string, error) { 33 | type Token struct { 34 | Token string `json:"access_token"` 35 | } 36 | 37 | data := struct { 38 | ClientId string `json:"client_id"` 39 | ClientSecret string `json:"client_secret"` 40 | Code string `json:"code"` 41 | }{ 42 | ClientId: app.ApiKey, 43 | ClientSecret: app.ApiSecret, 44 | Code: code, 45 | } 46 | 47 | client := NewClient(app, shopName, "") 48 | req, err := client.NewRequest("POST", "admin/oauth/access_token", data, nil) 49 | 50 | token := new(Token) 51 | err = client.Do(req, token) 52 | return token.Token, err 53 | } 54 | 55 | // Verify a message against a message HMAC 56 | func (app App) VerifyMessage(message, messageMAC string) bool { 57 | mac := hmac.New(sha256.New, []byte(app.ApiSecret)) 58 | mac.Write([]byte(message)) 59 | expectedMAC := mac.Sum(nil) 60 | 61 | // shopify HMAC is in hex so it needs to be decoded 62 | actualMac, _ := hex.DecodeString(messageMAC) 63 | 64 | return hmac.Equal(actualMac, expectedMAC) 65 | } 66 | 67 | // Verifying URL callback parameters. 68 | func (app App) VerifyAuthorizationURL(u *url.URL) (bool, error) { 69 | q := u.Query() 70 | messageMAC := q.Get("hmac") 71 | 72 | // Remove hmac and signature and leave the rest of the parameters alone. 73 | q.Del("hmac") 74 | q.Del("signature") 75 | 76 | message, err := url.QueryUnescape(q.Encode()) 77 | 78 | return app.VerifyMessage(message, messageMAC), err 79 | } 80 | 81 | // Verifies a webhook http request, sent by Shopify. 82 | // The body of the request is still readable after invoking the method. 83 | func (app App) VerifyWebhookRequest(httpRequest *http.Request) bool { 84 | shopifySha256 := httpRequest.Header.Get(shopifyChecksumHeader) 85 | actualMac := []byte(shopifySha256) 86 | 87 | mac := hmac.New(sha256.New, []byte(app.ApiSecret)) 88 | requestBody, _ := ioutil.ReadAll(httpRequest.Body) 89 | httpRequest.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody)) 90 | mac.Write(requestBody) 91 | macSum := mac.Sum(nil) 92 | expectedMac := []byte(base64.StdEncoding.EncodeToString(macSum)) 93 | 94 | return hmac.Equal(actualMac, expectedMac) 95 | } 96 | -------------------------------------------------------------------------------- /oauth_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "gopkg.in/jarcoal/httpmock.v1" 8 | ) 9 | 10 | func TestAppAuthorizeUrl(t *testing.T) { 11 | setup() 12 | defer teardown() 13 | 14 | cases := []struct { 15 | shopName string 16 | nonce string 17 | expected string 18 | }{ 19 | {"fooshop", "thenonce", "https://fooshop.myshopify.com/admin/oauth/authorize?client_id=apikey&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&scope=read_products&state=thenonce"}, 20 | } 21 | 22 | for _, c := range cases { 23 | actual := app.AuthorizeUrl(c.shopName, c.nonce) 24 | if actual != c.expected { 25 | t.Errorf("App.AuthorizeUrl(): expected %s, actual %s", c.expected, actual) 26 | } 27 | } 28 | } 29 | 30 | func TestAppGetAccessToken(t *testing.T) { 31 | setup() 32 | defer teardown() 33 | 34 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/oauth/access_token", 35 | httpmock.NewStringResponder(200, `{"access_token":"footoken"}`)) 36 | 37 | token, err := app.GetAccessToken("fooshop", "foocode") 38 | 39 | if err != nil { 40 | t.Fatalf("App.GetAccessToken(): %v", err) 41 | } 42 | 43 | expected := "footoken" 44 | if token != expected { 45 | t.Errorf("Token = %v, expected %v", token, expected) 46 | } 47 | } 48 | 49 | func TestAppVerifyAuthorizationURL(t *testing.T) { 50 | // These credentials are from the Shopify example page: 51 | // https://help.shopify.com/api/guides/authentication/oauth#verification 52 | urlOk, _ := url.Parse("http://example.com/callback?code=0907a61c0c8d55e99db179b68161bc00&hmac=4712bf92ffc2917d15a2f5a273e39f0116667419aa4b6ac0b3baaf26fa3c4d20&shop=some-shop.myshopify.com&signature=11813d1e7bbf4629edcda0628a3f7a20×tamp=1337178173") 53 | urlOkWithState, _ := url.Parse("http://example.com/callback?code=0907a61c0c8d55e99db179b68161bc00&hmac=7db6973c2aff68295ebcf354c2ce528a6b09aef1146baafccc2e0b369fff5f6d&shop=some-shop.myshopify.com&signature=11813d1e7bbf4629edcda0628a3f7a20×tamp=1337178173&state=abcd") 54 | urlNotOk, _ := url.Parse("http://example.com/callback?code=0907a61c0c8d55e99db179b68161bc00&hmac=4712bf92ffc2917d15a2f5a273e39f0116667419aa4b6ac0b3baaf26fa3c4d20&shop=some-shop.myshopify.com&signature=11813d1e7bbf4629edcda0628a3f7a20×tamp=133717817") 55 | 56 | cases := []struct { 57 | u *url.URL 58 | expected bool 59 | }{ 60 | {urlOk, true}, 61 | {urlOkWithState, true}, 62 | {urlNotOk, false}, 63 | } 64 | 65 | for _, c := range cases { 66 | actual, err := app.VerifyAuthorizationURL(c.u) 67 | if err != nil { 68 | t.Errorf("App.VerifyAuthorizationURL(..., %s) returned an error: %v", c.u, err) 69 | } 70 | if actual != c.expected { 71 | t.Errorf("App.VerifyAuthorizationURL(..., %s): expected %v, actual %v", c.u, c.expected, actual) 72 | } 73 | } 74 | } 75 | 76 | func TestVerifyWebhookRequest(t *testing.T) { 77 | setup() 78 | defer teardown() 79 | 80 | hmac := "hMTq0K2x7oyOjoBwGYeTj5oxfnaVYXzbanUG9aajpKI=" 81 | message := "my secret message" 82 | testClient := NewClient(App{}, "", "") 83 | req, err := testClient.NewRequest("GET", "", message, nil) 84 | if err != nil { 85 | t.Fatalf("Webhook.verify err = %v, expected true", err) 86 | } 87 | req.Header.Add("X-Shopify-Hmac-Sha256", hmac) 88 | 89 | isValid := app.VerifyWebhookRequest(req) 90 | 91 | if !isValid { 92 | t.Error("Webhook.verify could not verified message checksum") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /page.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const pagesBasePath = "admin/pages" 9 | const pagesResourceName = "pages" 10 | 11 | // PagesPageService is an interface for interacting with the pages 12 | // endpoints of the Shopify API. 13 | // See https://help.shopify.com/api/reference/online_store/page 14 | type PageService interface { 15 | List(interface{}) ([]Page, error) 16 | Count(interface{}) (int, error) 17 | Get(int, interface{}) (*Page, error) 18 | Create(Page) (*Page, error) 19 | Update(Page) (*Page, error) 20 | Delete(int) error 21 | 22 | // MetafieldsService used for Pages resource to communicate with Metafields 23 | // resource 24 | MetafieldsService 25 | } 26 | 27 | // PageServiceOp handles communication with the page related methods of the 28 | // Shopify API. 29 | type PageServiceOp struct { 30 | client *Client 31 | } 32 | 33 | // Page represents a Shopify page. 34 | type Page struct { 35 | ID int `json:"id"` 36 | Author string `json:"author"` 37 | Handle string `json:"handle"` 38 | Title string `json:"title"` 39 | CreatedAt *time.Time `json:"created_at"` 40 | UpdatedAt *time.Time `json:"updated_at"` 41 | BodyHTML string `json:"body_html"` 42 | TemplateSuffix string `json:"template_suffix"` 43 | PublishedAt *time.Time `json:"published_at"` 44 | ShopID int `json:"shop_id"` 45 | Metafields []Metafield `json:"metafields"` 46 | } 47 | 48 | // PageResource represents the result from the pages/X.json endpoint 49 | type PageResource struct { 50 | Page *Page `json:"page"` 51 | } 52 | 53 | // PagesResource represents the result from the pages.json endpoint 54 | type PagesResource struct { 55 | Pages []Page `json:"pages"` 56 | } 57 | 58 | // List pages 59 | func (s *PageServiceOp) List(options interface{}) ([]Page, error) { 60 | path := fmt.Sprintf("%s.json", pagesBasePath) 61 | resource := new(PagesResource) 62 | err := s.client.Get(path, resource, options) 63 | return resource.Pages, err 64 | } 65 | 66 | // Count pages 67 | func (s *PageServiceOp) Count(options interface{}) (int, error) { 68 | path := fmt.Sprintf("%s/count.json", pagesBasePath) 69 | return s.client.Count(path, options) 70 | } 71 | 72 | // Get individual page 73 | func (s *PageServiceOp) Get(pageID int, options interface{}) (*Page, error) { 74 | path := fmt.Sprintf("%s/%d.json", pagesBasePath, pageID) 75 | resource := new(PageResource) 76 | err := s.client.Get(path, resource, options) 77 | return resource.Page, err 78 | } 79 | 80 | // Create a new page 81 | func (s *PageServiceOp) Create(page Page) (*Page, error) { 82 | path := fmt.Sprintf("%s.json", pagesBasePath) 83 | wrappedData := PageResource{Page: &page} 84 | resource := new(PageResource) 85 | err := s.client.Post(path, wrappedData, resource) 86 | return resource.Page, err 87 | } 88 | 89 | // Update an existing page 90 | func (s *PageServiceOp) Update(page Page) (*Page, error) { 91 | path := fmt.Sprintf("%s/%d.json", pagesBasePath, page.ID) 92 | wrappedData := PageResource{Page: &page} 93 | resource := new(PageResource) 94 | err := s.client.Put(path, wrappedData, resource) 95 | return resource.Page, err 96 | } 97 | 98 | // Delete an existing page. 99 | func (s *PageServiceOp) Delete(pageID int) error { 100 | return s.client.Delete(fmt.Sprintf("%s/%d.json", pagesBasePath, pageID)) 101 | } 102 | 103 | // List metafields for a page 104 | func (s *PageServiceOp) ListMetafields(pageID int, options interface{}) ([]Metafield, error) { 105 | metafieldService := &MetafieldServiceOp{client: s.client, resource: pagesResourceName, resourceID: pageID} 106 | return metafieldService.List(options) 107 | } 108 | 109 | // Count metafields for a page 110 | func (s *PageServiceOp) CountMetafields(pageID int, options interface{}) (int, error) { 111 | metafieldService := &MetafieldServiceOp{client: s.client, resource: pagesResourceName, resourceID: pageID} 112 | return metafieldService.Count(options) 113 | } 114 | 115 | // Get individual metafield for a page 116 | func (s *PageServiceOp) GetMetafield(pageID int, metafieldID int, options interface{}) (*Metafield, error) { 117 | metafieldService := &MetafieldServiceOp{client: s.client, resource: pagesResourceName, resourceID: pageID} 118 | return metafieldService.Get(metafieldID, options) 119 | } 120 | 121 | // Create a new metafield for a page 122 | func (s *PageServiceOp) CreateMetafield(pageID int, metafield Metafield) (*Metafield, error) { 123 | metafieldService := &MetafieldServiceOp{client: s.client, resource: pagesResourceName, resourceID: pageID} 124 | return metafieldService.Create(metafield) 125 | } 126 | 127 | // Update an existing metafield for a page 128 | func (s *PageServiceOp) UpdateMetafield(pageID int, metafield Metafield) (*Metafield, error) { 129 | metafieldService := &MetafieldServiceOp{client: s.client, resource: pagesResourceName, resourceID: pageID} 130 | return metafieldService.Update(metafield) 131 | } 132 | 133 | // Delete an existing metafield for a page 134 | func (s *PageServiceOp) DeleteMetafield(pageID int, metafieldID int) error { 135 | metafieldService := &MetafieldServiceOp{client: s.client, resource: pagesResourceName, resourceID: pageID} 136 | return metafieldService.Delete(metafieldID) 137 | } 138 | -------------------------------------------------------------------------------- /page_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | httpmock "gopkg.in/jarcoal/httpmock.v1" 9 | ) 10 | 11 | func pageTests(t *testing.T, page Page) { 12 | // Check that ID is assigned to the returned page 13 | expectedInt := 1 14 | if page.ID != expectedInt { 15 | t.Errorf("Page.ID returned %+v, expected %+v", page.ID, expectedInt) 16 | } 17 | } 18 | 19 | func TestPageList(t *testing.T) { 20 | setup() 21 | defer teardown() 22 | 23 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/pages.json", 24 | httpmock.NewStringResponder(200, `{"pages": [{"id":1},{"id":2}]}`)) 25 | 26 | pages, err := client.Page.List(nil) 27 | if err != nil { 28 | t.Errorf("Page.List returned error: %v", err) 29 | } 30 | 31 | expected := []Page{{ID: 1}, {ID: 2}} 32 | if !reflect.DeepEqual(pages, expected) { 33 | t.Errorf("Page.List returned %+v, expected %+v", pages, expected) 34 | } 35 | } 36 | 37 | func TestPageCount(t *testing.T) { 38 | setup() 39 | defer teardown() 40 | 41 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/pages/count.json", 42 | httpmock.NewStringResponder(200, `{"count": 3}`)) 43 | 44 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/pages/count.json?created_at_min=2016-01-01T00%3A00%3A00Z", 45 | httpmock.NewStringResponder(200, `{"count": 2}`)) 46 | 47 | cnt, err := client.Page.Count(nil) 48 | if err != nil { 49 | t.Errorf("Page.Count returned error: %v", err) 50 | } 51 | 52 | expected := 3 53 | if cnt != expected { 54 | t.Errorf("Page.Count returned %d, expected %d", cnt, expected) 55 | } 56 | 57 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 58 | cnt, err = client.Page.Count(CountOptions{CreatedAtMin: date}) 59 | if err != nil { 60 | t.Errorf("Page.Count returned error: %v", err) 61 | } 62 | 63 | expected = 2 64 | if cnt != expected { 65 | t.Errorf("Page.Count returned %d, expected %d", cnt, expected) 66 | } 67 | } 68 | 69 | func TestPageGet(t *testing.T) { 70 | setup() 71 | defer teardown() 72 | 73 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/pages/1.json", 74 | httpmock.NewStringResponder(200, `{"page": {"id":1}}`)) 75 | 76 | page, err := client.Page.Get(1, nil) 77 | if err != nil { 78 | t.Errorf("Page.Get returned error: %v", err) 79 | } 80 | 81 | expected := &Page{ID: 1} 82 | if !reflect.DeepEqual(page, expected) { 83 | t.Errorf("Page.Get returned %+v, expected %+v", page, expected) 84 | } 85 | } 86 | 87 | func TestPageCreate(t *testing.T) { 88 | setup() 89 | defer teardown() 90 | 91 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/pages.json", 92 | httpmock.NewBytesResponder(200, loadFixture("page.json"))) 93 | 94 | page := Page{ 95 | Title: "404", 96 | BodyHTML: "NOT FOUND!<\\/strong>", 97 | } 98 | 99 | returnedPage, err := client.Page.Create(page) 100 | if err != nil { 101 | t.Errorf("Page.Create returned error: %v", err) 102 | } 103 | 104 | pageTests(t, *returnedPage) 105 | } 106 | 107 | func TestPageUpdate(t *testing.T) { 108 | setup() 109 | defer teardown() 110 | 111 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/pages/1.json", 112 | httpmock.NewBytesResponder(200, loadFixture("page.json"))) 113 | 114 | page := Page{ 115 | ID: 1, 116 | } 117 | 118 | returnedPage, err := client.Page.Update(page) 119 | if err != nil { 120 | t.Errorf("Page.Update returned error: %v", err) 121 | } 122 | 123 | pageTests(t, *returnedPage) 124 | } 125 | 126 | func TestPageDelete(t *testing.T) { 127 | setup() 128 | defer teardown() 129 | 130 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/pages/1.json", 131 | httpmock.NewStringResponder(200, "{}")) 132 | 133 | err := client.Page.Delete(1) 134 | if err != nil { 135 | t.Errorf("Page.Delete returned error: %v", err) 136 | } 137 | } 138 | 139 | func TestPageListMetafields(t *testing.T) { 140 | setup() 141 | defer teardown() 142 | 143 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/pages/1/metafields.json", 144 | httpmock.NewStringResponder(200, `{"metafields": [{"id":1},{"id":2}]}`)) 145 | 146 | metafields, err := client.Page.ListMetafields(1, nil) 147 | if err != nil { 148 | t.Errorf("Page.ListMetafields() returned error: %v", err) 149 | } 150 | 151 | expected := []Metafield{{ID: 1}, {ID: 2}} 152 | if !reflect.DeepEqual(metafields, expected) { 153 | t.Errorf("Page.ListMetafields() returned %+v, expected %+v", metafields, expected) 154 | } 155 | } 156 | 157 | func TestPageCountMetafields(t *testing.T) { 158 | setup() 159 | defer teardown() 160 | 161 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/pages/1/metafields/count.json", 162 | httpmock.NewStringResponder(200, `{"count": 3}`)) 163 | 164 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/pages/1/metafields/count.json?created_at_min=2016-01-01T00%3A00%3A00Z", 165 | httpmock.NewStringResponder(200, `{"count": 2}`)) 166 | 167 | cnt, err := client.Page.CountMetafields(1, nil) 168 | if err != nil { 169 | t.Errorf("Page.CountMetafields() returned error: %v", err) 170 | } 171 | 172 | expected := 3 173 | if cnt != expected { 174 | t.Errorf("Page.CountMetafields() returned %d, expected %d", cnt, expected) 175 | } 176 | 177 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 178 | cnt, err = client.Page.CountMetafields(1, CountOptions{CreatedAtMin: date}) 179 | if err != nil { 180 | t.Errorf("Page.CountMetafields() returned error: %v", err) 181 | } 182 | 183 | expected = 2 184 | if cnt != expected { 185 | t.Errorf("Page.CountMetafields() returned %d, expected %d", cnt, expected) 186 | } 187 | } 188 | 189 | func TestPageGetMetafield(t *testing.T) { 190 | setup() 191 | defer teardown() 192 | 193 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/pages/1/metafields/2.json", 194 | httpmock.NewStringResponder(200, `{"metafield": {"id":2}}`)) 195 | 196 | metafield, err := client.Page.GetMetafield(1, 2, nil) 197 | if err != nil { 198 | t.Errorf("Page.GetMetafield() returned error: %v", err) 199 | } 200 | 201 | expected := &Metafield{ID: 2} 202 | if !reflect.DeepEqual(metafield, expected) { 203 | t.Errorf("Page.GetMetafield() returned %+v, expected %+v", metafield, expected) 204 | } 205 | } 206 | 207 | func TestPageCreateMetafield(t *testing.T) { 208 | setup() 209 | defer teardown() 210 | 211 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/pages/1/metafields.json", 212 | httpmock.NewBytesResponder(200, loadFixture("metafield.json"))) 213 | 214 | metafield := Metafield{ 215 | Key: "app_key", 216 | Value: "app_value", 217 | ValueType: "string", 218 | Namespace: "affiliates", 219 | } 220 | 221 | returnedMetafield, err := client.Page.CreateMetafield(1, metafield) 222 | if err != nil { 223 | t.Errorf("Page.CreateMetafield() returned error: %v", err) 224 | } 225 | 226 | MetafieldTests(t, *returnedMetafield) 227 | } 228 | 229 | func TestPageUpdateMetafield(t *testing.T) { 230 | setup() 231 | defer teardown() 232 | 233 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/pages/1/metafields/2.json", 234 | httpmock.NewBytesResponder(200, loadFixture("metafield.json"))) 235 | 236 | metafield := Metafield{ 237 | ID: 2, 238 | Key: "app_key", 239 | Value: "app_value", 240 | ValueType: "string", 241 | Namespace: "affiliates", 242 | } 243 | 244 | returnedMetafield, err := client.Page.UpdateMetafield(1, metafield) 245 | if err != nil { 246 | t.Errorf("Page.UpdateMetafield() returned error: %v", err) 247 | } 248 | 249 | MetafieldTests(t, *returnedMetafield) 250 | } 251 | 252 | func TestPageDeleteMetafield(t *testing.T) { 253 | setup() 254 | defer teardown() 255 | 256 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/pages/1/metafields/2.json", 257 | httpmock.NewStringResponder(200, "{}")) 258 | 259 | err := client.Page.DeleteMetafield(1, 2) 260 | if err != nil { 261 | t.Errorf("Page.DeleteMetafield() returned error: %v", err) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /product.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const productsBasePath = "admin/products" 9 | const productsResourceName = "products" 10 | 11 | // ProductService is an interface for interfacing with the product endpoints 12 | // of the Shopify API. 13 | // See: https://help.shopify.com/api/reference/product 14 | type ProductService interface { 15 | List(interface{}) ([]Product, error) 16 | Count(interface{}) (int, error) 17 | Get(int, interface{}) (*Product, error) 18 | Create(Product) (*Product, error) 19 | Update(Product) (*Product, error) 20 | Delete(int) error 21 | 22 | // MetafieldsService used for Product resource to communicate with Metafields resource 23 | MetafieldsService 24 | } 25 | 26 | // ProductServiceOp handles communication with the product related methods of 27 | // the Shopify API. 28 | type ProductServiceOp struct { 29 | client *Client 30 | } 31 | 32 | // Product represents a Shopify product 33 | type Product struct { 34 | ID int `json:"id,omitempty"` 35 | Title string `json:"title,omitempty"` 36 | BodyHTML string `json:"body_html,omitempty"` 37 | Vendor string `json:"vendor,omitempty"` 38 | ProductType string `json:"product_type,omitempty"` 39 | Handle string `json:"handle,omitempty"` 40 | CreatedAt *time.Time `json:"created_at,omitempty"` 41 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 42 | PublishedAt *time.Time `json:"published_at,omitempty"` 43 | PublishedScope string `json:"published_scope,omitempty"` 44 | Tags string `json:"tags,omitempty"` 45 | Options []ProductOption `json:"options,omitempty"` 46 | Variants []Variant `json:"variants,omitempty"` 47 | Image Image `json:"image,omitempty"` 48 | Images []Image `json:"images,omitempty"` 49 | TemplateSuffix string `json:"template_suffix,omitempty"` 50 | MetafieldsGlobalTitleTag string `json:"metafields_global_title_tag,omitempty"` 51 | MetafieldsGlobalDescriptionTag string `json:"metafields_global_description_tag,omitempty"` 52 | Metafields []Metafield `json:"metafields,omitempty"` 53 | } 54 | 55 | // The options provided by Shopify 56 | type ProductOption struct { 57 | ID int `json:"id,omitempty"` 58 | ProductID int `json:"product_id,omitempty"` 59 | Name string `json:"name,omitempty"` 60 | Position int `json:"position,omitempty"` 61 | Values []string `json:"values,omitempty"` 62 | } 63 | 64 | // Represents the result from the products/X.json endpoint 65 | type ProductResource struct { 66 | Product *Product `json:"product"` 67 | } 68 | 69 | // Represents the result from the products.json endpoint 70 | type ProductsResource struct { 71 | Products []Product `json:"products"` 72 | } 73 | 74 | // List products 75 | func (s *ProductServiceOp) List(options interface{}) ([]Product, error) { 76 | path := fmt.Sprintf("%s.json", productsBasePath) 77 | resource := new(ProductsResource) 78 | err := s.client.Get(path, resource, options) 79 | return resource.Products, err 80 | } 81 | 82 | // Count products 83 | func (s *ProductServiceOp) Count(options interface{}) (int, error) { 84 | path := fmt.Sprintf("%s/count.json", productsBasePath) 85 | return s.client.Count(path, options) 86 | } 87 | 88 | // Get individual product 89 | func (s *ProductServiceOp) Get(productID int, options interface{}) (*Product, error) { 90 | path := fmt.Sprintf("%s/%d.json", productsBasePath, productID) 91 | resource := new(ProductResource) 92 | err := s.client.Get(path, resource, options) 93 | return resource.Product, err 94 | } 95 | 96 | // Create a new product 97 | func (s *ProductServiceOp) Create(product Product) (*Product, error) { 98 | path := fmt.Sprintf("%s.json", productsBasePath) 99 | wrappedData := ProductResource{Product: &product} 100 | resource := new(ProductResource) 101 | err := s.client.Post(path, wrappedData, resource) 102 | return resource.Product, err 103 | } 104 | 105 | // Update an existing product 106 | func (s *ProductServiceOp) Update(product Product) (*Product, error) { 107 | path := fmt.Sprintf("%s/%d.json", productsBasePath, product.ID) 108 | wrappedData := ProductResource{Product: &product} 109 | resource := new(ProductResource) 110 | err := s.client.Put(path, wrappedData, resource) 111 | return resource.Product, err 112 | } 113 | 114 | // Delete an existing product 115 | func (s *ProductServiceOp) Delete(productID int) error { 116 | return s.client.Delete(fmt.Sprintf("%s/%d.json", productsBasePath, productID)) 117 | } 118 | 119 | // List metafields for a product 120 | func (s *ProductServiceOp) ListMetafields(productID int, options interface{}) ([]Metafield, error) { 121 | metafieldService := &MetafieldServiceOp{client: s.client, resource: productsResourceName, resourceID: productID} 122 | return metafieldService.List(options) 123 | } 124 | 125 | // Count metafields for a product 126 | func (s *ProductServiceOp) CountMetafields(productID int, options interface{}) (int, error) { 127 | metafieldService := &MetafieldServiceOp{client: s.client, resource: productsResourceName, resourceID: productID} 128 | return metafieldService.Count(options) 129 | } 130 | 131 | // Get individual metafield for a product 132 | func (s *ProductServiceOp) GetMetafield(productID int, metafieldID int, options interface{}) (*Metafield, error) { 133 | metafieldService := &MetafieldServiceOp{client: s.client, resource: productsResourceName, resourceID: productID} 134 | return metafieldService.Get(metafieldID, options) 135 | } 136 | 137 | // Create a new metafield for a product 138 | func (s *ProductServiceOp) CreateMetafield(productID int, metafield Metafield) (*Metafield, error) { 139 | metafieldService := &MetafieldServiceOp{client: s.client, resource: productsResourceName, resourceID: productID} 140 | return metafieldService.Create(metafield) 141 | } 142 | 143 | // Update an existing metafield for a product 144 | func (s *ProductServiceOp) UpdateMetafield(productID int, metafield Metafield) (*Metafield, error) { 145 | metafieldService := &MetafieldServiceOp{client: s.client, resource: productsResourceName, resourceID: productID} 146 | return metafieldService.Update(metafield) 147 | } 148 | 149 | // // Delete an existing metafield for a product 150 | func (s *ProductServiceOp) DeleteMetafield(productID int, metafieldID int) error { 151 | metafieldService := &MetafieldServiceOp{client: s.client, resource: productsResourceName, resourceID: productID} 152 | return metafieldService.Delete(metafieldID) 153 | } 154 | -------------------------------------------------------------------------------- /product_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | httpmock "gopkg.in/jarcoal/httpmock.v1" 9 | ) 10 | 11 | func productTests(t *testing.T, product Product) { 12 | // Check that ID is assigned to the returned product 13 | expectedInt := 1071559748 14 | if product.ID != expectedInt { 15 | t.Errorf("Product.ID returned %+v, expected %+v", product.ID, expectedInt) 16 | } 17 | } 18 | 19 | func TestProductList(t *testing.T) { 20 | setup() 21 | defer teardown() 22 | 23 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products.json", 24 | httpmock.NewStringResponder(200, `{"products": [{"id":1},{"id":2}]}`)) 25 | 26 | products, err := client.Product.List(nil) 27 | if err != nil { 28 | t.Errorf("Product.List returned error: %v", err) 29 | } 30 | 31 | expected := []Product{{ID: 1}, {ID: 2}} 32 | if !reflect.DeepEqual(products, expected) { 33 | t.Errorf("Product.List returned %+v, expected %+v", products, expected) 34 | } 35 | } 36 | 37 | func TestProductCount(t *testing.T) { 38 | setup() 39 | defer teardown() 40 | 41 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/count.json", 42 | httpmock.NewStringResponder(200, `{"count": 3}`)) 43 | 44 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/count.json?created_at_min=2016-01-01T00%3A00%3A00Z", 45 | httpmock.NewStringResponder(200, `{"count": 2}`)) 46 | 47 | cnt, err := client.Product.Count(nil) 48 | if err != nil { 49 | t.Errorf("Product.Count returned error: %v", err) 50 | } 51 | 52 | expected := 3 53 | if cnt != expected { 54 | t.Errorf("Product.Count returned %d, expected %d", cnt, expected) 55 | } 56 | 57 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 58 | cnt, err = client.Product.Count(CountOptions{CreatedAtMin: date}) 59 | if err != nil { 60 | t.Errorf("Product.Count returned error: %v", err) 61 | } 62 | 63 | expected = 2 64 | if cnt != expected { 65 | t.Errorf("Product.Count returned %d, expected %d", cnt, expected) 66 | } 67 | } 68 | 69 | func TestProductGet(t *testing.T) { 70 | setup() 71 | defer teardown() 72 | 73 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1.json", 74 | httpmock.NewStringResponder(200, `{"product": {"id":1}}`)) 75 | 76 | product, err := client.Product.Get(1, nil) 77 | if err != nil { 78 | t.Errorf("Product.Get returned error: %v", err) 79 | } 80 | 81 | expected := &Product{ID: 1} 82 | if !reflect.DeepEqual(product, expected) { 83 | t.Errorf("Product.Get returned %+v, expected %+v", product, expected) 84 | } 85 | } 86 | 87 | func TestProductCreate(t *testing.T) { 88 | setup() 89 | defer teardown() 90 | 91 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/products.json", 92 | httpmock.NewBytesResponder(200, loadFixture("product.json"))) 93 | 94 | product := Product{ 95 | Title: "Burton Custom Freestyle 151", 96 | BodyHTML: "Good snowboard!<\\/strong>", 97 | Vendor: "Burton", 98 | ProductType: "Snowboard", 99 | } 100 | 101 | returnedProduct, err := client.Product.Create(product) 102 | if err != nil { 103 | t.Errorf("Product.Create returned error: %v", err) 104 | } 105 | 106 | productTests(t, *returnedProduct) 107 | } 108 | 109 | func TestProductUpdate(t *testing.T) { 110 | setup() 111 | defer teardown() 112 | 113 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/products/1.json", 114 | httpmock.NewBytesResponder(200, loadFixture("product.json"))) 115 | 116 | product := Product{ 117 | ID: 1, 118 | ProductType: "Skateboard", 119 | } 120 | 121 | returnedProduct, err := client.Product.Update(product) 122 | if err != nil { 123 | t.Errorf("Product.Update returned error: %v", err) 124 | } 125 | 126 | productTests(t, *returnedProduct) 127 | } 128 | 129 | func TestProductDelete(t *testing.T) { 130 | setup() 131 | defer teardown() 132 | 133 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/products/1.json", 134 | httpmock.NewStringResponder(200, "{}")) 135 | 136 | err := client.Product.Delete(1) 137 | if err != nil { 138 | t.Errorf("Product.Delete returned error: %v", err) 139 | } 140 | } 141 | 142 | func TestProductListMetafields(t *testing.T) { 143 | setup() 144 | defer teardown() 145 | 146 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/metafields.json", 147 | httpmock.NewStringResponder(200, `{"metafields": [{"id":1},{"id":2}]}`)) 148 | 149 | metafields, err := client.Product.ListMetafields(1, nil) 150 | if err != nil { 151 | t.Errorf("Product.ListMetafields() returned error: %v", err) 152 | } 153 | 154 | expected := []Metafield{{ID: 1}, {ID: 2}} 155 | if !reflect.DeepEqual(metafields, expected) { 156 | t.Errorf("Product.ListMetafields() returned %+v, expected %+v", metafields, expected) 157 | } 158 | } 159 | 160 | func TestProductCountMetafields(t *testing.T) { 161 | setup() 162 | defer teardown() 163 | 164 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/metafields/count.json", 165 | httpmock.NewStringResponder(200, `{"count": 3}`)) 166 | 167 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/metafields/count.json?created_at_min=2016-01-01T00%3A00%3A00Z", 168 | httpmock.NewStringResponder(200, `{"count": 2}`)) 169 | 170 | cnt, err := client.Product.CountMetafields(1, nil) 171 | if err != nil { 172 | t.Errorf("Product.CountMetafields() returned error: %v", err) 173 | } 174 | 175 | expected := 3 176 | if cnt != expected { 177 | t.Errorf("Product.CountMetafields() returned %d, expected %d", cnt, expected) 178 | } 179 | 180 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 181 | cnt, err = client.Product.CountMetafields(1, CountOptions{CreatedAtMin: date}) 182 | if err != nil { 183 | t.Errorf("Product.CountMetafields() returned error: %v", err) 184 | } 185 | 186 | expected = 2 187 | if cnt != expected { 188 | t.Errorf("Product.CountMetafields() returned %d, expected %d", cnt, expected) 189 | } 190 | } 191 | 192 | func TestProductGetMetafield(t *testing.T) { 193 | setup() 194 | defer teardown() 195 | 196 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/metafields/2.json", 197 | httpmock.NewStringResponder(200, `{"metafield": {"id":2}}`)) 198 | 199 | metafield, err := client.Product.GetMetafield(1, 2, nil) 200 | if err != nil { 201 | t.Errorf("Product.GetMetafield() returned error: %v", err) 202 | } 203 | 204 | expected := &Metafield{ID: 2} 205 | if !reflect.DeepEqual(metafield, expected) { 206 | t.Errorf("Product.GetMetafield() returned %+v, expected %+v", metafield, expected) 207 | } 208 | } 209 | 210 | func TestProductCreateMetafield(t *testing.T) { 211 | setup() 212 | defer teardown() 213 | 214 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/products/1/metafields.json", 215 | httpmock.NewBytesResponder(200, loadFixture("metafield.json"))) 216 | 217 | metafield := Metafield{ 218 | Key: "app_key", 219 | Value: "app_value", 220 | ValueType: "string", 221 | Namespace: "affiliates", 222 | } 223 | 224 | returnedMetafield, err := client.Product.CreateMetafield(1, metafield) 225 | if err != nil { 226 | t.Errorf("Product.CreateMetafield() returned error: %v", err) 227 | } 228 | 229 | MetafieldTests(t, *returnedMetafield) 230 | } 231 | 232 | func TestProductUpdateMetafield(t *testing.T) { 233 | setup() 234 | defer teardown() 235 | 236 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/products/1/metafields/2.json", 237 | httpmock.NewBytesResponder(200, loadFixture("metafield.json"))) 238 | 239 | metafield := Metafield{ 240 | ID: 2, 241 | Key: "app_key", 242 | Value: "app_value", 243 | ValueType: "string", 244 | Namespace: "affiliates", 245 | } 246 | 247 | returnedMetafield, err := client.Product.UpdateMetafield(1, metafield) 248 | if err != nil { 249 | t.Errorf("Product.UpdateMetafield() returned error: %v", err) 250 | } 251 | 252 | MetafieldTests(t, *returnedMetafield) 253 | } 254 | 255 | func TestProductDeleteMetafield(t *testing.T) { 256 | setup() 257 | defer teardown() 258 | 259 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/products/1/metafields/2.json", 260 | httpmock.NewStringResponder(200, "{}")) 261 | 262 | err := client.Product.DeleteMetafield(1, 2) 263 | if err != nil { 264 | t.Errorf("Product.DeleteMetafield() returned error: %v", err) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /recurringapplicationcharge.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | const recurringApplicationChargesBasePath = "admin/recurring_application_charges" 12 | 13 | // RecurringApplicationChargeService is an interface for interacting with the 14 | // RecurringApplicationCharge endpoints of the Shopify API. 15 | // See https://help.shopify.com/api/reference/billing/recurringapplicationcharge 16 | type RecurringApplicationChargeService interface { 17 | Create(RecurringApplicationCharge) (*RecurringApplicationCharge, error) 18 | Get(int, interface{}) (*RecurringApplicationCharge, error) 19 | List(interface{}) ([]RecurringApplicationCharge, error) 20 | Activate(RecurringApplicationCharge) (*RecurringApplicationCharge, error) 21 | Delete(int) error 22 | Update(int, int) (*RecurringApplicationCharge, error) 23 | } 24 | 25 | // RecurringApplicationChargeServiceOp handles communication with the 26 | // RecurringApplicationCharge related methods of the Shopify API. 27 | type RecurringApplicationChargeServiceOp struct { 28 | client *Client 29 | } 30 | 31 | // RecurringApplicationCharge represents a Shopify RecurringApplicationCharge. 32 | type RecurringApplicationCharge struct { 33 | APIClientID int `json:"api_client_id"` 34 | ActivatedOn *time.Time `json:"activated_on"` 35 | BalanceRemaining *decimal.Decimal `json:"balance_remaining"` 36 | BalanceUsed *decimal.Decimal `json:"balance_used"` 37 | BillingOn *time.Time `json:"billing_on"` 38 | CancelledOn *time.Time `json:"cancelled_on"` 39 | CappedAmount *decimal.Decimal `json:"capped_amount"` 40 | ConfirmationURL string `json:"confirmation_url"` 41 | CreatedAt *time.Time `json:"created_at"` 42 | DecoratedReturnURL string `json:"decorated_return_url"` 43 | ID int `json:"id"` 44 | Name string `json:"name"` 45 | Price *decimal.Decimal `json:"price"` 46 | ReturnURL string `json:"return_url"` 47 | RiskLevel *decimal.Decimal `json:"risk_level"` 48 | Status string `json:"status"` 49 | Terms string `json:"terms"` 50 | Test *bool `json:"test"` 51 | TrialDays int `json:"trial_days"` 52 | TrialEndsOn *time.Time `json:"trial_ends_on"` 53 | UpdateCappedAmountURL string `json:"update_capped_amount_url"` 54 | UpdatedAt *time.Time `json:"updated_at"` 55 | } 56 | 57 | func parse(dest **time.Time, data *string) error { 58 | if data == nil { 59 | return nil 60 | } 61 | // This is what API doc says: "2013-06-27T08:48:27-04:00" 62 | format := time.RFC3339 63 | if len(*data) == 10 { 64 | // This is how the date looks. 65 | format = "2006-01-02" 66 | } 67 | t, err := time.Parse(format, *data) 68 | if err != nil { 69 | return err 70 | } 71 | *dest = &t 72 | return nil 73 | } 74 | 75 | func (r *RecurringApplicationCharge) UnmarshalJSON(data []byte) error { 76 | // This is a workaround for the API returning incomplete results: 77 | // https://ecommerce.shopify.com/c/shopify-apis-and-technology/t/-523203 78 | // For a longer explanation of the hack check: 79 | // http://choly.ca/post/go-json-marshalling/ 80 | type alias RecurringApplicationCharge 81 | aux := &struct { 82 | ActivatedOn *string `json:"activated_on"` 83 | BillingOn *string `json:"billing_on"` 84 | CancelledOn *string `json:"cancelled_on"` 85 | CreatedAt *string `json:"created_at"` 86 | TrialEndsOn *string `json:"trial_ends_on"` 87 | UpdatedAt *string `json:"updated_at"` 88 | *alias 89 | }{alias: (*alias)(r)} 90 | 91 | if err := json.Unmarshal(data, &aux); err != nil { 92 | return err 93 | } 94 | if err := parse(&r.ActivatedOn, aux.ActivatedOn); err != nil { 95 | return err 96 | } 97 | if err := parse(&r.BillingOn, aux.BillingOn); err != nil { 98 | return err 99 | } 100 | if err := parse(&r.CancelledOn, aux.CancelledOn); err != nil { 101 | return err 102 | } 103 | if err := parse(&r.CreatedAt, aux.CreatedAt); err != nil { 104 | return err 105 | } 106 | if err := parse(&r.TrialEndsOn, aux.TrialEndsOn); err != nil { 107 | return err 108 | } 109 | if err := parse(&r.UpdatedAt, aux.UpdatedAt); err != nil { 110 | return err 111 | } 112 | return nil 113 | } 114 | 115 | // RecurringApplicationChargeResource represents the result from the 116 | // admin/recurring_application_charges{/X{/activate.json}.json}.json endpoints. 117 | type RecurringApplicationChargeResource struct { 118 | Charge *RecurringApplicationCharge `json:"recurring_application_charge"` 119 | } 120 | 121 | // RecurringApplicationChargesResource represents the result from the 122 | // admin/recurring_application_charges.json endpoint. 123 | type RecurringApplicationChargesResource struct { 124 | Charges []RecurringApplicationCharge `json:"recurring_application_charges"` 125 | } 126 | 127 | // Create creates new recurring application charge. 128 | func (r *RecurringApplicationChargeServiceOp) Create(charge RecurringApplicationCharge) ( 129 | *RecurringApplicationCharge, error) { 130 | 131 | path := fmt.Sprintf("%s.json", recurringApplicationChargesBasePath) 132 | wrappedData := RecurringApplicationChargeResource{Charge: &charge} 133 | resource := &RecurringApplicationChargeResource{} 134 | err := r.client.Post(path, wrappedData, resource) 135 | return resource.Charge, err 136 | } 137 | 138 | // Get gets individual recurring application charge. 139 | func (r *RecurringApplicationChargeServiceOp) Get(chargeID int, options interface{}) ( 140 | *RecurringApplicationCharge, error) { 141 | 142 | path := fmt.Sprintf("%s/%d.json", recurringApplicationChargesBasePath, chargeID) 143 | resource := &RecurringApplicationChargeResource{} 144 | err := r.client.Get(path, resource, options) 145 | return resource.Charge, err 146 | } 147 | 148 | // List gets all recurring application charges. 149 | func (r *RecurringApplicationChargeServiceOp) List(options interface{}) ( 150 | []RecurringApplicationCharge, error) { 151 | 152 | path := fmt.Sprintf("%s.json", recurringApplicationChargesBasePath) 153 | resource := &RecurringApplicationChargesResource{} 154 | err := r.client.Get(path, resource, options) 155 | return resource.Charges, err 156 | } 157 | 158 | // Activate activates recurring application charge. 159 | func (r *RecurringApplicationChargeServiceOp) Activate(charge RecurringApplicationCharge) ( 160 | *RecurringApplicationCharge, error) { 161 | 162 | path := fmt.Sprintf("%s/%d/activate.json", recurringApplicationChargesBasePath, charge.ID) 163 | wrappedData := RecurringApplicationChargeResource{Charge: &charge} 164 | resource := &RecurringApplicationChargeResource{} 165 | err := r.client.Post(path, wrappedData, resource) 166 | return resource.Charge, err 167 | } 168 | 169 | // Delete deletes recurring application charge. 170 | func (r *RecurringApplicationChargeServiceOp) Delete(chargeID int) error { 171 | return r.client.Delete(fmt.Sprintf("%s/%d.json", recurringApplicationChargesBasePath, chargeID)) 172 | } 173 | 174 | // Update updates recurring application charge. 175 | func (r *RecurringApplicationChargeServiceOp) Update(chargeID, newCappedAmount int) ( 176 | *RecurringApplicationCharge, error) { 177 | 178 | path := fmt.Sprintf("%s/%d/customize.json?recurring_application_charge[capped_amount]=%d", 179 | recurringApplicationChargesBasePath, chargeID, newCappedAmount) 180 | resource := &RecurringApplicationChargeResource{} 181 | err := r.client.Put(path, nil, resource) 182 | return resource.Charge, err 183 | } 184 | -------------------------------------------------------------------------------- /redirect.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const redirectsBasePath = "admin/redirects" 8 | 9 | // RedirectService is an interface for interacting with the redirects 10 | // endpoints of the Shopify API. 11 | // See https://help.shopify.com/api/reference/online_store/redirect 12 | type RedirectService interface { 13 | List(interface{}) ([]Redirect, error) 14 | Count(interface{}) (int, error) 15 | Get(int, interface{}) (*Redirect, error) 16 | Create(Redirect) (*Redirect, error) 17 | Update(Redirect) (*Redirect, error) 18 | Delete(int) error 19 | } 20 | 21 | // RedirectServiceOp handles communication with the redirect related methods of the 22 | // Shopify API. 23 | type RedirectServiceOp struct { 24 | client *Client 25 | } 26 | 27 | // Redirect represents a Shopify redirect. 28 | type Redirect struct { 29 | ID int `json:"id"` 30 | Path string `json:"path"` 31 | Target string `json:"target"` 32 | } 33 | 34 | // RedirectResource represents the result from the redirects/X.json endpoint 35 | type RedirectResource struct { 36 | Redirect *Redirect `json:"redirect"` 37 | } 38 | 39 | // RedirectsResource represents the result from the redirects.json endpoint 40 | type RedirectsResource struct { 41 | Redirects []Redirect `json:"redirects"` 42 | } 43 | 44 | // List redirects 45 | func (s *RedirectServiceOp) List(options interface{}) ([]Redirect, error) { 46 | path := fmt.Sprintf("%s.json", redirectsBasePath) 47 | resource := new(RedirectsResource) 48 | err := s.client.Get(path, resource, options) 49 | return resource.Redirects, err 50 | } 51 | 52 | // Count redirects 53 | func (s *RedirectServiceOp) Count(options interface{}) (int, error) { 54 | path := fmt.Sprintf("%s/count.json", redirectsBasePath) 55 | return s.client.Count(path, options) 56 | } 57 | 58 | // Get individual redirect 59 | func (s *RedirectServiceOp) Get(redirectID int, options interface{}) (*Redirect, error) { 60 | path := fmt.Sprintf("%s/%d.json", redirectsBasePath, redirectID) 61 | resource := new(RedirectResource) 62 | err := s.client.Get(path, resource, options) 63 | return resource.Redirect, err 64 | } 65 | 66 | // Create a new redirect 67 | func (s *RedirectServiceOp) Create(redirect Redirect) (*Redirect, error) { 68 | path := fmt.Sprintf("%s.json", redirectsBasePath) 69 | wrappedData := RedirectResource{Redirect: &redirect} 70 | resource := new(RedirectResource) 71 | err := s.client.Post(path, wrappedData, resource) 72 | return resource.Redirect, err 73 | } 74 | 75 | // Update an existing redirect 76 | func (s *RedirectServiceOp) Update(redirect Redirect) (*Redirect, error) { 77 | path := fmt.Sprintf("%s/%d.json", redirectsBasePath, redirect.ID) 78 | wrappedData := RedirectResource{Redirect: &redirect} 79 | resource := new(RedirectResource) 80 | err := s.client.Put(path, wrappedData, resource) 81 | return resource.Redirect, err 82 | } 83 | 84 | // Delete an existing redirect. 85 | func (s *RedirectServiceOp) Delete(redirectID int) error { 86 | return s.client.Delete(fmt.Sprintf("%s/%d.json", redirectsBasePath, redirectID)) 87 | } 88 | -------------------------------------------------------------------------------- /redirect_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | httpmock "gopkg.in/jarcoal/httpmock.v1" 9 | ) 10 | 11 | func redirectTests(t *testing.T, redirect Redirect) { 12 | // Check that ID is assigned to the returned redirect 13 | expectedInt := 1 14 | if redirect.ID != expectedInt { 15 | t.Errorf("Redirect.ID returned %+v, expected %+v", redirect.ID, expectedInt) 16 | } 17 | } 18 | 19 | func TestRedirectList(t *testing.T) { 20 | setup() 21 | defer teardown() 22 | 23 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/redirects.json", 24 | httpmock.NewStringResponder(200, `{"redirects": [{"id":1},{"id":2}]}`)) 25 | 26 | redirects, err := client.Redirect.List(nil) 27 | if err != nil { 28 | t.Errorf("Redirect.List returned error: %v", err) 29 | } 30 | 31 | expected := []Redirect{{ID: 1}, {ID: 2}} 32 | if !reflect.DeepEqual(redirects, expected) { 33 | t.Errorf("Redirect.List returned %+v, expected %+v", redirects, expected) 34 | } 35 | } 36 | 37 | func TestRedirectCount(t *testing.T) { 38 | setup() 39 | defer teardown() 40 | 41 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/redirects/count.json", 42 | httpmock.NewStringResponder(200, `{"count": 3}`)) 43 | 44 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/redirects/count.json?created_at_min=2016-01-01T00%3A00%3A00Z", 45 | httpmock.NewStringResponder(200, `{"count": 2}`)) 46 | 47 | cnt, err := client.Redirect.Count(nil) 48 | if err != nil { 49 | t.Errorf("Redirect.Count returned error: %v", err) 50 | } 51 | 52 | expected := 3 53 | if cnt != expected { 54 | t.Errorf("Redirect.Count returned %d, expected %d", cnt, expected) 55 | } 56 | 57 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 58 | cnt, err = client.Redirect.Count(CountOptions{CreatedAtMin: date}) 59 | if err != nil { 60 | t.Errorf("Redirect.Count returned error: %v", err) 61 | } 62 | 63 | expected = 2 64 | if cnt != expected { 65 | t.Errorf("Redirect.Count returned %d, expected %d", cnt, expected) 66 | } 67 | } 68 | 69 | func TestRedirectGet(t *testing.T) { 70 | setup() 71 | defer teardown() 72 | 73 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/redirects/1.json", 74 | httpmock.NewStringResponder(200, `{"redirect": {"id":1}}`)) 75 | 76 | redirect, err := client.Redirect.Get(1, nil) 77 | if err != nil { 78 | t.Errorf("Redirect.Get returned error: %v", err) 79 | } 80 | 81 | expected := &Redirect{ID: 1} 82 | if !reflect.DeepEqual(redirect, expected) { 83 | t.Errorf("Redirect.Get returned %+v, expected %+v", redirect, expected) 84 | } 85 | } 86 | 87 | func TestRedirectCreate(t *testing.T) { 88 | setup() 89 | defer teardown() 90 | 91 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/redirects.json", 92 | httpmock.NewBytesResponder(200, loadFixture("redirect.json"))) 93 | 94 | redirect := Redirect{ 95 | Path: "/from", 96 | Target: "/to", 97 | } 98 | 99 | returnedRedirect, err := client.Redirect.Create(redirect) 100 | if err != nil { 101 | t.Errorf("Redirect.Create returned error: %v", err) 102 | } 103 | 104 | redirectTests(t, *returnedRedirect) 105 | } 106 | 107 | func TestRedirectUpdate(t *testing.T) { 108 | setup() 109 | defer teardown() 110 | 111 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/redirects/1.json", 112 | httpmock.NewBytesResponder(200, loadFixture("redirect.json"))) 113 | 114 | redirect := Redirect{ 115 | ID: 1, 116 | } 117 | 118 | returnedRedirect, err := client.Redirect.Update(redirect) 119 | if err != nil { 120 | t.Errorf("Redirect.Update returned error: %v", err) 121 | } 122 | 123 | redirectTests(t, *returnedRedirect) 124 | } 125 | 126 | func TestRedirectDelete(t *testing.T) { 127 | setup() 128 | defer teardown() 129 | 130 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/redirects/1.json", 131 | httpmock.NewStringResponder(200, "{}")) 132 | 133 | err := client.Redirect.Delete(1) 134 | if err != nil { 135 | t.Errorf("Redirect.Delete returned error: %v", err) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /scripttag.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const scriptTagsBasePath = "admin/script_tags" 9 | 10 | // ScriptTagService is an interface for interfacing with the ScriptTag endpoints 11 | // of the Shopify API. 12 | // See: https://help.shopify.com/api/reference/scripttag 13 | type ScriptTagService interface { 14 | List(interface{}) ([]ScriptTag, error) 15 | Count(interface{}) (int, error) 16 | Get(int, interface{}) (*ScriptTag, error) 17 | Create(ScriptTag) (*ScriptTag, error) 18 | Update(ScriptTag) (*ScriptTag, error) 19 | Delete(int) error 20 | } 21 | 22 | // ScriptTagServiceOp handles communication with the shop related methods of the 23 | // Shopify API. 24 | type ScriptTagServiceOp struct { 25 | client *Client 26 | } 27 | 28 | // ScriptTag represents a Shopify ScriptTag. 29 | type ScriptTag struct { 30 | CreatedAt *time.Time `json:"created_at"` 31 | Event string `json:"event"` 32 | ID int `json:"id"` 33 | Src string `json:"src"` 34 | DisplayScope string `json:"display_scope"` 35 | UpdatedAt *time.Time `json:"updated_at"` 36 | } 37 | 38 | // The options provided by Shopify. 39 | type ScriptTagOption struct { 40 | Limit int `url:"limit,omitempty"` 41 | Page int `url:"page,omitempty"` 42 | SinceID int `url:"since_id,omitempty"` 43 | CreatedAtMin time.Time `url:"created_at_min,omitempty"` 44 | CreatedAtMax time.Time `url:"created_at_max,omitempty"` 45 | UpdatedAtMin time.Time `url:"updated_at_min,omitempty"` 46 | UpdatedAtMax time.Time `url:"updated_at_max,omitempty"` 47 | Src string `url:"src,omitempty"` 48 | Fields string `url:"fields,omitempty"` 49 | } 50 | 51 | // ScriptTagsResource represents the result from the admin/script_tags.json 52 | // endpoint. 53 | type ScriptTagsResource struct { 54 | ScriptTags []ScriptTag `json:"script_tags"` 55 | } 56 | 57 | // ScriptTagResource represents the result from the 58 | // admin/script_tags/{#script_tag_id}.json endpoint. 59 | type ScriptTagResource struct { 60 | ScriptTag *ScriptTag `json:"script_tag"` 61 | } 62 | 63 | // List script tags 64 | func (s *ScriptTagServiceOp) List(options interface{}) ([]ScriptTag, error) { 65 | path := fmt.Sprintf("%s.json", scriptTagsBasePath) 66 | resource := &ScriptTagsResource{} 67 | err := s.client.Get(path, resource, options) 68 | return resource.ScriptTags, err 69 | } 70 | 71 | // Count script tags 72 | func (s *ScriptTagServiceOp) Count(options interface{}) (int, error) { 73 | path := fmt.Sprintf("%s/count.json", scriptTagsBasePath) 74 | return s.client.Count(path, options) 75 | } 76 | 77 | // Get individual script tag 78 | func (s *ScriptTagServiceOp) Get(tagID int, options interface{}) (*ScriptTag, error) { 79 | path := fmt.Sprintf("%s/%d.json", scriptTagsBasePath, tagID) 80 | resource := &ScriptTagResource{} 81 | err := s.client.Get(path, resource, options) 82 | return resource.ScriptTag, err 83 | } 84 | 85 | // Create a new script tag 86 | func (s *ScriptTagServiceOp) Create(tag ScriptTag) (*ScriptTag, error) { 87 | path := fmt.Sprintf("%s.json", scriptTagsBasePath) 88 | wrappedData := ScriptTagResource{ScriptTag: &tag} 89 | resource := &ScriptTagResource{} 90 | err := s.client.Post(path, wrappedData, resource) 91 | return resource.ScriptTag, err 92 | } 93 | 94 | // Update an existing script tag 95 | func (s *ScriptTagServiceOp) Update(tag ScriptTag) (*ScriptTag, error) { 96 | path := fmt.Sprintf("%s/%d.json", scriptTagsBasePath, tag.ID) 97 | wrappedData := ScriptTagResource{ScriptTag: &tag} 98 | resource := &ScriptTagResource{} 99 | err := s.client.Put(path, wrappedData, resource) 100 | return resource.ScriptTag, err 101 | } 102 | 103 | // Delete an existing script tag 104 | func (s *ScriptTagServiceOp) Delete(tagID int) error { 105 | return s.client.Delete(fmt.Sprintf("%s/%d.json", scriptTagsBasePath, tagID)) 106 | } 107 | -------------------------------------------------------------------------------- /scripttag_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "gopkg.in/jarcoal/httpmock.v1" 8 | ) 9 | 10 | func TestScriptTagList(t *testing.T) { 11 | setup() 12 | defer teardown() 13 | 14 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/script_tags.json", 15 | httpmock.NewStringResponder(200, `{"script_tags": [{"id": 1},{"id": 2}]}`)) 16 | 17 | scriptTags, err := client.ScriptTag.List(nil) 18 | if err != nil { 19 | t.Errorf("ScriptTag.List returned error: %v", err) 20 | } 21 | 22 | expected := []ScriptTag{{ID: 1}, {ID: 2}} 23 | if !reflect.DeepEqual(scriptTags, expected) { 24 | t.Errorf("ScriptTag.List returned %+v, expected %+v", scriptTags, expected) 25 | } 26 | } 27 | 28 | func TestScriptTagCount(t *testing.T) { 29 | setup() 30 | defer teardown() 31 | 32 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/script_tags/count.json", 33 | httpmock.NewStringResponder(200, `{"count": 3}`)) 34 | 35 | cnt, err := client.ScriptTag.Count(nil) 36 | if err != nil { 37 | t.Errorf("ScriptTag.Count returned error: %v", err) 38 | } 39 | 40 | expected := 3 41 | if cnt != expected { 42 | t.Errorf("ScriptTag.Count returned %d, expected %d", cnt, expected) 43 | } 44 | } 45 | 46 | func TestScriptTagGet(t *testing.T) { 47 | setup() 48 | defer teardown() 49 | 50 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/script_tags/1.json", 51 | httpmock.NewStringResponder(200, `{"script_tag": {"id": 1}}`)) 52 | 53 | scriptTag, err := client.ScriptTag.Get(1, nil) 54 | if err != nil { 55 | t.Errorf("ScriptTag.Get returned error: %v", err) 56 | } 57 | 58 | expected := &ScriptTag{ID: 1} 59 | if !reflect.DeepEqual(scriptTag, expected) { 60 | t.Errorf("ScriptTag.Get returned %+v, expected %+v", scriptTag, expected) 61 | } 62 | } 63 | 64 | func scriptTagTests(t *testing.T, tag ScriptTag) { 65 | expected := 870402688 66 | if tag.ID != expected { 67 | t.Errorf("tag.ID is %+v, expected %+v", tag.ID, expected) 68 | } 69 | } 70 | 71 | func TestScriptTagCreate(t *testing.T) { 72 | setup() 73 | defer teardown() 74 | 75 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/script_tags.json", 76 | httpmock.NewBytesResponder(200, loadFixture("script_tags.json"))) 77 | 78 | tag0 := ScriptTag{ 79 | Src: "https://djavaskripped.org/fancy.js", 80 | Event: "onload", 81 | DisplayScope: "all", 82 | } 83 | 84 | returnedTag, err := client.ScriptTag.Create(tag0) 85 | if err != nil { 86 | t.Errorf("ScriptTag.Create returned error: %v", err) 87 | } 88 | scriptTagTests(t, *returnedTag) 89 | } 90 | 91 | func TestScriptTagUpdate(t *testing.T) { 92 | setup() 93 | defer teardown() 94 | 95 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/script_tags/1.json", 96 | httpmock.NewBytesResponder(200, loadFixture("script_tags.json"))) 97 | 98 | tag := ScriptTag{ 99 | ID: 1, 100 | Src: "https://djavaskripped.org/fancy.js", 101 | } 102 | 103 | returnedTag, err := client.ScriptTag.Update(tag) 104 | if err != nil { 105 | t.Errorf("ScriptTag.Update returned error: %v", err) 106 | } 107 | scriptTagTests(t, *returnedTag) 108 | } 109 | 110 | func TestScriptTagDelete(t *testing.T) { 111 | setup() 112 | defer teardown() 113 | 114 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/script_tags/1.json", 115 | httpmock.NewStringResponder(200, "{}")) 116 | 117 | if err := client.ScriptTag.Delete(1); err != nil { 118 | t.Errorf("ScriptTag.Delete returned error: %v", err) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /shop.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import "time" 4 | 5 | // ShopService is an interface for interfacing with the shop endpoint of the 6 | // Shopify API. 7 | // See: https://help.shopify.com/api/reference/shop 8 | type ShopService interface { 9 | Get(options interface{}) (*Shop, error) 10 | } 11 | 12 | // ShopServiceOp handles communication with the shop related methods of the 13 | // Shopify API. 14 | type ShopServiceOp struct { 15 | client *Client 16 | } 17 | 18 | // Shop represents a Shopify shop 19 | type Shop struct { 20 | ID int `json:"id"` 21 | Name string `json:"name"` 22 | ShopOwner string `json:"shop_owner"` 23 | Email string `json:"email"` 24 | CustomerEmail string `json:"customer_email"` 25 | CreatedAt *time.Time `json:"created_at"` 26 | UpdatedAt *time.Time `json:"updated_at"` 27 | Address1 string `json:"address1"` 28 | Address2 string `json:"address2"` 29 | City string `json:"city"` 30 | Country string `json:"country"` 31 | CountryCode string `json:"country_code"` 32 | CountryName string `json:"country_name"` 33 | Currency string `json:"currency"` 34 | Domain string `json:"domain"` 35 | Latitude float64 `json:"latitude"` 36 | Longitude float64 `json:"longitude"` 37 | Phone string `json:"phone"` 38 | Province string `json:"province"` 39 | ProvinceCode string `json:"province_code"` 40 | Zip string `json:"zip"` 41 | MoneyFormat string `json:"money_format"` 42 | MoneyWithCurrencyFormat string `json:"money_with_currency_format"` 43 | WeightUnit string `json:"weight_unit"` 44 | MyshopifyDomain string `json:"myshopify_domain"` 45 | PlanName string `json:"plan_name"` 46 | PlanDisplayName string `json:"plan_display_name"` 47 | PasswordEnabled bool `json:"password_enabled"` 48 | PrimaryLocale string `json:"primary_locale"` 49 | Timezone string `json:"timezone"` 50 | IanaTimezone string `json:"iana_timezone"` 51 | ForceSSL bool `json:"force_ssl"` 52 | TaxShipping bool `json:"tax_shipping"` 53 | TaxesIncluded bool `json:"taxes_included"` 54 | HasStorefront bool `json:"has_storefront"` 55 | HasDiscounts bool `json:"has_discounts"` 56 | HasGiftcards bool `json:"has_gift_cards"` 57 | SetupRequire bool `json:"setup_required"` 58 | CountyTaxes bool `json:"county_taxes"` 59 | CheckoutAPISupported bool `json:"checkout_api_supported"` 60 | } 61 | 62 | // Represents the result from the admin/shop.json endpoint 63 | type ShopResource struct { 64 | Shop *Shop `json:"shop"` 65 | } 66 | 67 | // Get shop 68 | func (s *ShopServiceOp) Get(options interface{}) (*Shop, error) { 69 | resource := new(ShopResource) 70 | err := s.client.Get("admin/shop.json", resource, options) 71 | return resource.Shop, err 72 | } 73 | -------------------------------------------------------------------------------- /shop_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "gopkg.in/jarcoal/httpmock.v1" 8 | ) 9 | 10 | func TestShopGet(t *testing.T) { 11 | setup() 12 | defer teardown() 13 | 14 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/shop.json", 15 | httpmock.NewBytesResponder(200, loadFixture("shop.json"))) 16 | 17 | shop, err := client.Shop.Get(nil) 18 | if err != nil { 19 | t.Errorf("Shop.Get returned error: %v", err) 20 | } 21 | 22 | // Check that dates are parsed 23 | d := time.Date(2007, time.December, 31, 19, 00, 00, 0, time.UTC) 24 | if !d.Equal(*shop.CreatedAt) { 25 | t.Errorf("Shop.CreatedAt returned %+v, expected %+v", shop.CreatedAt, d) 26 | } 27 | 28 | // Test a few fields 29 | cases := []struct { 30 | field string 31 | expected interface{} 32 | actual interface{} 33 | }{ 34 | {"ID", 690933842, shop.ID}, 35 | {"ShopOwner", "Steve Jobs", shop.ShopOwner}, 36 | {"Address1", "1 Infinite Loop", shop.Address1}, 37 | {"Name", "Apple Computers", shop.Name}, 38 | {"Email", "steve@apple.com", shop.Email}, 39 | {"HasStorefront", true, shop.HasStorefront}, 40 | } 41 | 42 | for _, c := range cases { 43 | if c.expected != c.actual { 44 | t.Errorf("Shop.%v returned %v, expected %v", c.field, c.actual, c.expected) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /smartcollection.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const smartCollectionsBasePath = "admin/smart_collections" 9 | const smartCollectionsResourceName = "collections" 10 | 11 | // SmartCollectionService is an interface for interacting with the smart 12 | // collection endpoints of the Shopify API. 13 | // See https://help.shopify.com/api/reference/smartcollection 14 | type SmartCollectionService interface { 15 | List(interface{}) ([]SmartCollection, error) 16 | Count(interface{}) (int, error) 17 | Get(int, interface{}) (*SmartCollection, error) 18 | Create(SmartCollection) (*SmartCollection, error) 19 | Update(SmartCollection) (*SmartCollection, error) 20 | Delete(int) error 21 | 22 | // MetafieldsService used for SmartCollection resource to communicate with Metafields resource 23 | MetafieldsService 24 | } 25 | 26 | // SmartCollectionServiceOp handles communication with the smart collection 27 | // related methods of the Shopify API. 28 | type SmartCollectionServiceOp struct { 29 | client *Client 30 | } 31 | 32 | type Rule struct { 33 | Column string `json:"column"` 34 | Relation string `json:"relation"` 35 | Condition string `json:"condition"` 36 | } 37 | 38 | // SmartCollection represents a Shopify smart collection. 39 | type SmartCollection struct { 40 | ID int `json:"id"` 41 | Handle string `json:"handle"` 42 | Title string `json:"title"` 43 | UpdatedAt *time.Time `json:"updated_at"` 44 | BodyHTML string `json:"body_html"` 45 | SortOrder string `json:"sort_order"` 46 | TemplateSuffix string `json:"template_suffix"` 47 | Image Image `json:"image"` 48 | Published bool `json:"published"` 49 | PublishedAt *time.Time `json:"published_at"` 50 | PublishedScope string `json:"published_scope"` 51 | Rules []Rule `json:"rules"` 52 | Disjunctive bool `json:"disjunctive"` 53 | Metafields []Metafield `json:"metafields,omitempty"` 54 | } 55 | 56 | // SmartCollectionResource represents the result from the smart_collections/X.json endpoint 57 | type SmartCollectionResource struct { 58 | Collection *SmartCollection `json:"smart_collection"` 59 | } 60 | 61 | // SmartCollectionsResource represents the result from the smart_collections.json endpoint 62 | type SmartCollectionsResource struct { 63 | Collections []SmartCollection `json:"smart_collections"` 64 | } 65 | 66 | // List smart collections 67 | func (s *SmartCollectionServiceOp) List(options interface{}) ([]SmartCollection, error) { 68 | path := fmt.Sprintf("%s.json", smartCollectionsBasePath) 69 | resource := new(SmartCollectionsResource) 70 | err := s.client.Get(path, resource, options) 71 | return resource.Collections, err 72 | } 73 | 74 | // Count smart collections 75 | func (s *SmartCollectionServiceOp) Count(options interface{}) (int, error) { 76 | path := fmt.Sprintf("%s/count.json", smartCollectionsBasePath) 77 | return s.client.Count(path, options) 78 | } 79 | 80 | // Get individual smart collection 81 | func (s *SmartCollectionServiceOp) Get(collectionID int, options interface{}) (*SmartCollection, error) { 82 | path := fmt.Sprintf("%s/%d.json", smartCollectionsBasePath, collectionID) 83 | resource := new(SmartCollectionResource) 84 | err := s.client.Get(path, resource, options) 85 | return resource.Collection, err 86 | } 87 | 88 | // Create a new smart collection 89 | // See Image for the details of the Image creation for a collection. 90 | func (s *SmartCollectionServiceOp) Create(collection SmartCollection) (*SmartCollection, error) { 91 | path := fmt.Sprintf("%s.json", smartCollectionsBasePath) 92 | wrappedData := SmartCollectionResource{Collection: &collection} 93 | resource := new(SmartCollectionResource) 94 | err := s.client.Post(path, wrappedData, resource) 95 | return resource.Collection, err 96 | } 97 | 98 | // Update an existing smart collection 99 | func (s *SmartCollectionServiceOp) Update(collection SmartCollection) (*SmartCollection, error) { 100 | path := fmt.Sprintf("%s/%d.json", smartCollectionsBasePath, collection.ID) 101 | wrappedData := SmartCollectionResource{Collection: &collection} 102 | resource := new(SmartCollectionResource) 103 | err := s.client.Put(path, wrappedData, resource) 104 | return resource.Collection, err 105 | } 106 | 107 | // Delete an existing smart collection. 108 | func (s *SmartCollectionServiceOp) Delete(collectionID int) error { 109 | return s.client.Delete(fmt.Sprintf("%s/%d.json", smartCollectionsBasePath, collectionID)) 110 | } 111 | 112 | // List metafields for a smart collection 113 | func (s *SmartCollectionServiceOp) ListMetafields(smartCollectionID int, options interface{}) ([]Metafield, error) { 114 | metafieldService := &MetafieldServiceOp{client: s.client, resource: smartCollectionsResourceName, resourceID: smartCollectionID} 115 | return metafieldService.List(options) 116 | } 117 | 118 | // Count metafields for a smart collection 119 | func (s *SmartCollectionServiceOp) CountMetafields(smartCollectionID int, options interface{}) (int, error) { 120 | metafieldService := &MetafieldServiceOp{client: s.client, resource: smartCollectionsResourceName, resourceID: smartCollectionID} 121 | return metafieldService.Count(options) 122 | } 123 | 124 | // Get individual metafield for a smart collection 125 | func (s *SmartCollectionServiceOp) GetMetafield(smartCollectionID int, metafieldID int, options interface{}) (*Metafield, error) { 126 | metafieldService := &MetafieldServiceOp{client: s.client, resource: smartCollectionsResourceName, resourceID: smartCollectionID} 127 | return metafieldService.Get(metafieldID, options) 128 | } 129 | 130 | // Create a new metafield for a smart collection 131 | func (s *SmartCollectionServiceOp) CreateMetafield(smartCollectionID int, metafield Metafield) (*Metafield, error) { 132 | metafieldService := &MetafieldServiceOp{client: s.client, resource: smartCollectionsResourceName, resourceID: smartCollectionID} 133 | return metafieldService.Create(metafield) 134 | } 135 | 136 | // Update an existing metafield for a smart collection 137 | func (s *SmartCollectionServiceOp) UpdateMetafield(smartCollectionID int, metafield Metafield) (*Metafield, error) { 138 | metafieldService := &MetafieldServiceOp{client: s.client, resource: smartCollectionsResourceName, resourceID: smartCollectionID} 139 | return metafieldService.Update(metafield) 140 | } 141 | 142 | // // Delete an existing metafield for a smart collection 143 | func (s *SmartCollectionServiceOp) DeleteMetafield(smartCollectionID int, metafieldID int) error { 144 | metafieldService := &MetafieldServiceOp{client: s.client, resource: smartCollectionsResourceName, resourceID: smartCollectionID} 145 | return metafieldService.Delete(metafieldID) 146 | } 147 | -------------------------------------------------------------------------------- /theme.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const themesBasePath = "admin/themes" 9 | 10 | // Options for theme list 11 | type ThemeListOptions struct { 12 | ListOptions 13 | Role string `url:"role,omitempty"` 14 | } 15 | 16 | // ThemeService is an interface for interfacing with the themes endpoints 17 | // of the Shopify API. 18 | // See: https://help.shopify.com/api/reference/theme 19 | type ThemeService interface { 20 | List(interface{}) ([]Theme, error) 21 | } 22 | 23 | // ThemeServiceOp handles communication with the theme related methods of 24 | // the Shopify API. 25 | type ThemeServiceOp struct { 26 | client *Client 27 | } 28 | 29 | // Theme represents a Shopify theme 30 | type Theme struct { 31 | ID int `json:"id"` 32 | Name string `json:"string"` 33 | Previewable bool `json:"previewable"` 34 | Processing bool `json:"processing"` 35 | Role string `json:"role"` 36 | ThemeStoreID int `json:"theme_store_id"` 37 | CreatedAt *time.Time `json:"created_at"` 38 | UpdatedAt *time.Time `json:"updated_at"` 39 | } 40 | 41 | // ThemesResource is the result from the themes.json endpoint 42 | type ThemesResource struct { 43 | Themes []Theme `json:"themes"` 44 | } 45 | 46 | // List all themes 47 | func (s *ThemeServiceOp) List(options interface{}) ([]Theme, error) { 48 | path := fmt.Sprintf("%s.json", themesBasePath) 49 | resource := new(ThemesResource) 50 | err := s.client.Get(path, resource, options) 51 | return resource.Themes, err 52 | } 53 | -------------------------------------------------------------------------------- /theme_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "gopkg.in/jarcoal/httpmock.v1" 8 | ) 9 | 10 | func TestThemeList(t *testing.T) { 11 | setup() 12 | defer teardown() 13 | 14 | httpmock.RegisterResponder( 15 | "GET", 16 | "https://fooshop.myshopify.com/admin/themes.json", 17 | httpmock.NewStringResponder( 18 | 200, 19 | `{"themes": [{"id":1},{"id":2}]}`, 20 | ), 21 | ) 22 | 23 | httpmock.RegisterResponder( 24 | "GET", 25 | "https://fooshop.myshopify.com/admin/themes.json?role=main", 26 | httpmock.NewStringResponder( 27 | 200, 28 | `{"themes": [{"id":1}]}`, 29 | ), 30 | ) 31 | 32 | themes, err := client.Theme.List(nil) 33 | if err != nil { 34 | t.Errorf("Theme.List returned error: %v", err) 35 | } 36 | 37 | expected := []Theme{{ID: 1}, {ID: 2}} 38 | if !reflect.DeepEqual(themes, expected) { 39 | t.Errorf("Theme.List returned %+v, expected %+v", themes, expected) 40 | } 41 | 42 | themes, err = client.Theme.List(ThemeListOptions{Role: "main"}) 43 | if err != nil { 44 | t.Errorf("Theme.List returned error: %v", err) 45 | } 46 | 47 | expected = []Theme{{ID: 1}} 48 | if !reflect.DeepEqual(themes, expected) { 49 | t.Errorf("Theme.List returned %+v, expected %+v", themes, expected) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import "fmt" 4 | 5 | // TransactionService is an interface for interfacing with the transactions endpoints of 6 | // the Shopify API. 7 | // See: https://help.shopify.com/api/reference/transaction 8 | type TransactionService interface { 9 | List(int, interface{}) ([]Transaction, error) 10 | Count(int, interface{}) (int, error) 11 | Get(int, int, interface{}) (*Transaction, error) 12 | Create(int, Transaction) (*Transaction, error) 13 | } 14 | 15 | // TransactionServiceOp handles communication with the transaction related methods of the 16 | // Shopify API. 17 | type TransactionServiceOp struct { 18 | client *Client 19 | } 20 | 21 | // TransactionResource represents the result from the orders/X/transactions/Y.json endpoint 22 | type TransactionResource struct { 23 | Transaction *Transaction `json:"transaction"` 24 | } 25 | 26 | // TransactionsResource represents the result from the orders/X/transactions.json endpoint 27 | type TransactionsResource struct { 28 | Transactions []Transaction `json:"transactions"` 29 | } 30 | 31 | // List transactions 32 | func (s *TransactionServiceOp) List(orderID int, options interface{}) ([]Transaction, error) { 33 | path := fmt.Sprintf("%s/%d/transactions.json", ordersBasePath, orderID) 34 | resource := new(TransactionsResource) 35 | err := s.client.Get(path, resource, options) 36 | return resource.Transactions, err 37 | } 38 | 39 | // Count transactions 40 | func (s *TransactionServiceOp) Count(orderID int, options interface{}) (int, error) { 41 | path := fmt.Sprintf("%s/%d/transactions/count.json", ordersBasePath, orderID) 42 | return s.client.Count(path, options) 43 | } 44 | 45 | // Get individual transaction 46 | func (s *TransactionServiceOp) Get(orderID int, transactionID int, options interface{}) (*Transaction, error) { 47 | path := fmt.Sprintf("%s/%d/transactions/%d.json", ordersBasePath, orderID, transactionID) 48 | resource := new(TransactionResource) 49 | err := s.client.Get(path, resource, options) 50 | return resource.Transaction, err 51 | } 52 | 53 | // Create a new transaction 54 | func (s *TransactionServiceOp) Create(orderID int, transaction Transaction) (*Transaction, error) { 55 | path := fmt.Sprintf("%s/%d/transactions.json", ordersBasePath, orderID) 56 | wrappedData := TransactionResource{Transaction: &transaction} 57 | resource := new(TransactionResource) 58 | err := s.client.Post(path, wrappedData, resource) 59 | return resource.Transaction, err 60 | } 61 | -------------------------------------------------------------------------------- /transaction_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/shopspring/decimal" 8 | httpmock "gopkg.in/jarcoal/httpmock.v1" 9 | ) 10 | 11 | func TransactionTests(t *testing.T, transaction Transaction) { 12 | // Check that the ID is assigned to the returned transaction 13 | expectedID := 389404469 14 | if transaction.ID != expectedID { 15 | t.Errorf("Transaction.ID returned %+v, expected %+v", transaction.ID, expectedID) 16 | } 17 | 18 | // Check that the OrderID value is assigned to the returned transaction 19 | expectedOrderID := 450789469 20 | if transaction.OrderID != expectedOrderID { 21 | t.Errorf("Transaction.OrderID returned %+v, expected %+v", transaction.OrderID, expectedOrderID) 22 | } 23 | 24 | // Check that the Amount value is assigned to the returned transaction 25 | expectedAmount, _ := decimal.NewFromString("409.94") 26 | if !transaction.Amount.Equals(expectedAmount) { 27 | t.Errorf("Transaction.Amount returned %+v, expected %+v", transaction.Amount, expectedAmount) 28 | } 29 | 30 | // Check that the Kind value is assigned to the returned transaction 31 | expectedKind := "authorization" 32 | if transaction.Kind != expectedKind { 33 | t.Errorf("Transaction.Kind returned %+v, expected %+v", transaction.Kind, expectedKind) 34 | } 35 | 36 | // Check that the Gateway value is assigned to the returned transaction 37 | expectedGateway := "bogus" 38 | if transaction.Gateway != expectedGateway { 39 | t.Errorf("Transaction.Gateway returned %+v, expected %+v", transaction.Gateway, expectedGateway) 40 | } 41 | 42 | // Check that the Status value is assigned to the returned transaction 43 | expectedStatus := "success" 44 | if transaction.Status != expectedStatus { 45 | t.Errorf("Transaction.Status returned %+v, expected %+v", transaction.Status, expectedStatus) 46 | } 47 | 48 | // Check that the Message value is assigned to the returned transaction 49 | expectedMessage := "Bogus Gateway: Forced success" 50 | if transaction.Message != expectedMessage { 51 | t.Errorf("Transaction.Message returned %+v, expected %+v", transaction.Message, expectedMessage) 52 | } 53 | 54 | // Check that the CreatedAt value is assigned to the returned transaction 55 | expectedCreatedAt := time.Date(2017, time.July, 24, 19, 9, 43, 0, time.UTC) 56 | if !expectedCreatedAt.Equal(*transaction.CreatedAt) { 57 | t.Errorf("Transaction.CreatedAt returned %+v, expected %+v", transaction.CreatedAt, expectedCreatedAt) 58 | } 59 | 60 | // Check that the Test value is assigned to the returned transaction 61 | expectedTest := true 62 | if transaction.Test != expectedTest { 63 | t.Errorf("Transaction.Test returned %+v, expected %+v", transaction.Test, expectedTest) 64 | } 65 | 66 | // Check that the Authorization value is assigned to the returned transaction 67 | expectedAuthorization := "authorization-key" 68 | if transaction.Authorization != expectedAuthorization { 69 | t.Errorf("Transaction.Authorization returned %+v, expected %+v", transaction.Authorization, expectedAuthorization) 70 | } 71 | 72 | // Check that the Currency value is assigned to the returned transaction 73 | expectedCurrency := "USD" 74 | if transaction.Currency != expectedCurrency { 75 | t.Errorf("Transaction.Currency returned %+v, expected %+v", transaction.Currency, expectedCurrency) 76 | } 77 | 78 | // Check that the LocationID value is assigned to the returned transaction 79 | var expectedLocationID *int 80 | if transaction.LocationID != expectedLocationID { 81 | t.Errorf("Transaction.LocationID returned %+v, expected %+v", transaction.LocationID, expectedLocationID) 82 | } 83 | 84 | // Check that the UserID value is assigned to the returned transaction 85 | var expectedUserID *int 86 | if transaction.UserID != expectedUserID { 87 | t.Errorf("Transaction.UserID returned %+v, expected %+v", transaction.UserID, expectedUserID) 88 | } 89 | 90 | // Check that the ParentID value is assigned to the returned transaction 91 | var expectedParentID *int 92 | if transaction.ParentID != expectedParentID { 93 | t.Errorf("Transaction.ParentID returned %+v, expected %+v", transaction.ParentID, expectedParentID) 94 | } 95 | 96 | // Check that the DeviceID value is assigned to the returned transaction 97 | var expectedDeviceID *int 98 | if transaction.DeviceID != expectedDeviceID { 99 | t.Errorf("Transacion.DeviceID returned %+v, expected %+v", transaction.DeviceID, expectedDeviceID) 100 | } 101 | 102 | // Check that the ErrorCode value is assigned to the returned transaction 103 | var expectedErrorCode string 104 | if transaction.ErrorCode != expectedErrorCode { 105 | t.Errorf("Transaction.ErrorCode returned %+v, expected %+v", transaction.ErrorCode, expectedErrorCode) 106 | } 107 | 108 | // Check that the SourceName value is assigned to the returned transaction 109 | expectedSourceName := "web" 110 | if transaction.SourceName != expectedSourceName { 111 | t.Errorf("Transaction.SourceName returned %+v, expected %+v", transaction.SourceName, expectedSourceName) 112 | } 113 | 114 | // Check that the PaymentDetails value is assigned to the returned transaction 115 | var nilString string 116 | expectedPaymentDetails := PaymentDetails{ 117 | AVSResultCode: nilString, 118 | CreditCardBin: nilString, 119 | CVVResultCode: nilString, 120 | CreditCardNumber: "•••• •••• •••• 4242", 121 | CreditCardCompany: "Visa", 122 | } 123 | if transaction.PaymentDetails.AVSResultCode != expectedPaymentDetails.AVSResultCode { 124 | t.Errorf("Transaction.PaymentDetails.AVSResultCode returned %+v, expected %+v", 125 | transaction.PaymentDetails.AVSResultCode, expectedPaymentDetails.AVSResultCode) 126 | } 127 | } 128 | 129 | func TestTransactionList(t *testing.T) { 130 | setup() 131 | defer teardown() 132 | 133 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/1/transactions.json", 134 | httpmock.NewBytesResponder(200, loadFixture("transactions.json"))) 135 | 136 | transactions, err := client.Transaction.List(1, nil) 137 | if err != nil { 138 | t.Errorf("Transaction.List returned error: %v", err) 139 | } 140 | 141 | for _, transaction := range transactions { 142 | TransactionTests(t, transaction) 143 | } 144 | } 145 | 146 | func TestTransactionCount(t *testing.T) { 147 | setup() 148 | defer teardown() 149 | 150 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/1/transactions/count.json", 151 | httpmock.NewStringResponder(200, `{"count": 2}`)) 152 | 153 | cnt, err := client.Transaction.Count(1, nil) 154 | if err != nil { 155 | t.Errorf("Transaction.Count returned error: %v", err) 156 | } 157 | 158 | expected := 2 159 | if cnt != expected { 160 | t.Errorf("Transaction.Count returned %d, expected %d", cnt, expected) 161 | } 162 | } 163 | 164 | func TestTransactionGet(t *testing.T) { 165 | setup() 166 | defer teardown() 167 | 168 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/orders/1/transactions/1.json", 169 | httpmock.NewBytesResponder(200, loadFixture("transaction.json"))) 170 | 171 | transaction, err := client.Transaction.Get(1, 1, nil) 172 | if err != nil { 173 | t.Errorf("Transaction.Get returned error: %v", err) 174 | } 175 | 176 | TransactionTests(t, *transaction) 177 | } 178 | 179 | func TestTransactionCreate(t *testing.T) { 180 | setup() 181 | defer teardown() 182 | 183 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/orders/1/transactions.json", 184 | httpmock.NewBytesResponder(200, loadFixture("transaction.json"))) 185 | 186 | amount := decimal.NewFromFloat(409.94) 187 | 188 | transaction := Transaction{ 189 | Amount: &amount, 190 | } 191 | result, err := client.Transaction.Create(1, transaction) 192 | if err != nil { 193 | t.Errorf("Transaction.Create returned error: %+v", err) 194 | } 195 | TransactionTests(t, *result) 196 | } 197 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Return the full shop name, including .myshopify.com 9 | func ShopFullName(name string) string { 10 | name = strings.TrimSpace(name) 11 | name = strings.Trim(name, ".") 12 | if strings.Contains(name, "myshopify.com") { 13 | return name 14 | } 15 | return name + ".myshopify.com" 16 | } 17 | 18 | // Return the short shop name, excluding .myshopify.com 19 | func ShopShortName(name string) string { 20 | // Convert to fullname and remove the myshopify part. Perhaps not the most 21 | // performant solution, but then we don't have to repeat all the trims here 22 | // :-) 23 | return strings.Replace(ShopFullName(name), ".myshopify.com", "", -1) 24 | } 25 | 26 | // Return the Shop's base url. 27 | func ShopBaseUrl(name string) string { 28 | name = ShopFullName(name) 29 | return fmt.Sprintf("https://%s", name) 30 | } 31 | 32 | // Return the prefix for a metafield path 33 | func MetafieldPathPrefix(resource string, resourceID int) string { 34 | var prefix string 35 | if resource == "" { 36 | prefix = fmt.Sprintf("admin/metafields") 37 | } else { 38 | prefix = fmt.Sprintf("admin/%s/%d/metafields", resource, resourceID) 39 | } 40 | return prefix 41 | } 42 | 43 | // Return the prefix for a fulfillment path 44 | func FulfillmentPathPrefix(resource string, resourceID int) string { 45 | var prefix string 46 | if resource == "" { 47 | prefix = fmt.Sprintf("admin/fulfillments") 48 | } else { 49 | prefix = fmt.Sprintf("admin/%s/%d/fulfillments", resource, resourceID) 50 | } 51 | return prefix 52 | } 53 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import "testing" 4 | 5 | func TestShopFullName(t *testing.T) { 6 | cases := []struct { 7 | in, expected string 8 | }{ 9 | {"myshop", "myshop.myshopify.com"}, 10 | {"myshop.", "myshop.myshopify.com"}, 11 | {" myshop", "myshop.myshopify.com"}, 12 | {"myshop ", "myshop.myshopify.com"}, 13 | {"myshop \n", "myshop.myshopify.com"}, 14 | {"myshop.myshopify.com", "myshop.myshopify.com"}, 15 | } 16 | 17 | for _, c := range cases { 18 | actual := ShopFullName(c.in) 19 | if actual != c.expected { 20 | t.Errorf("ShopFullName(%s): expected %s, actual %s", c.in, c.expected, actual) 21 | } 22 | } 23 | } 24 | 25 | func TestShopShortName(t *testing.T) { 26 | cases := []struct { 27 | in, expected string 28 | }{ 29 | {"myshop", "myshop"}, 30 | {"myshop.", "myshop"}, 31 | {" myshop", "myshop"}, 32 | {"myshop ", "myshop"}, 33 | {"myshop \n", "myshop"}, 34 | {"myshop.myshopify.com", "myshop"}, 35 | {".myshop.myshopify.com.", "myshop"}, 36 | } 37 | 38 | for _, c := range cases { 39 | actual := ShopShortName(c.in) 40 | if actual != c.expected { 41 | t.Errorf("ShopShortName(%s): expected %s, actual %s", c.in, c.expected, actual) 42 | } 43 | } 44 | } 45 | 46 | func TestShopBaseUrl(t *testing.T) { 47 | cases := []struct { 48 | in, expected string 49 | }{ 50 | {"myshop", "https://myshop.myshopify.com"}, 51 | {"myshop.", "https://myshop.myshopify.com"}, 52 | {" myshop", "https://myshop.myshopify.com"}, 53 | {"myshop ", "https://myshop.myshopify.com"}, 54 | {"myshop \n", "https://myshop.myshopify.com"}, 55 | {"myshop.myshopify.com", "https://myshop.myshopify.com"}, 56 | } 57 | 58 | for _, c := range cases { 59 | actual := ShopBaseUrl(c.in) 60 | if actual != c.expected { 61 | t.Errorf("ShopBaseUrl(%s): expected %s, actual %s", c.in, c.expected, actual) 62 | } 63 | } 64 | } 65 | 66 | func TestMetafieldPathPrefix(t *testing.T) { 67 | cases := []struct { 68 | resource string 69 | resourceID int 70 | expected string 71 | }{ 72 | {"", 0, "admin/metafields"}, 73 | {"products", 123, "admin/products/123/metafields"}, 74 | } 75 | 76 | for _, c := range cases { 77 | actual := MetafieldPathPrefix(c.resource, c.resourceID) 78 | if actual != c.expected { 79 | t.Errorf("MetafieldPathPrefix(%s, %d): expected %s, actual %s", c.resource, c.resourceID, c.expected, actual) 80 | } 81 | } 82 | } 83 | 84 | func TestFulfillmentPathPrefix(t *testing.T) { 85 | cases := []struct { 86 | resource string 87 | resourceID int 88 | expected string 89 | }{ 90 | {"", 0, "admin/fulfillments"}, 91 | {"orders", 123, "admin/orders/123/fulfillments"}, 92 | } 93 | 94 | for _, c := range cases { 95 | actual := FulfillmentPathPrefix(c.resource, c.resourceID) 96 | if actual != c.expected { 97 | t.Errorf("FulfillmentPathPrefix(%s, %d): expected %s, actual %s", c.resource, c.resourceID, c.expected, actual) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /variant.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | const variantsBasePath = "admin/variants" 11 | 12 | // VariantService is an interface for interacting with the variant endpoints 13 | // of the Shopify API. 14 | // See https://help.shopify.com/api/reference/product_variant 15 | type VariantService interface { 16 | List(int, interface{}) ([]Variant, error) 17 | Count(int, interface{}) (int, error) 18 | Get(int, interface{}) (*Variant, error) 19 | Create(int, Variant) (*Variant, error) 20 | Update(Variant) (*Variant, error) 21 | Delete(int, int) error 22 | } 23 | 24 | // VariantServiceOp handles communication with the variant related methods of 25 | // the Shopify API. 26 | type VariantServiceOp struct { 27 | client *Client 28 | } 29 | 30 | // Variant represents a Shopify variant 31 | type Variant struct { 32 | ID int `json:"id,omitempty"` 33 | ProductID int `json:"product_id,omitempty"` 34 | Title string `json:"title,omitempty"` 35 | Sku string `json:"sku,omitempty"` 36 | Position int `json:"position,omitempty"` 37 | Grams int `json:"grams,omitempty"` 38 | InventoryPolicy string `json:"inventory_policy,omitempty"` 39 | Price *decimal.Decimal `json:"price,omitempty"` 40 | CompareAtPrice *decimal.Decimal `json:"compare_at_price,omitempty"` 41 | FulfillmentService string `json:"fulfillment_service,omitempty"` 42 | InventoryManagement string `json:"inventory_management,omitempty"` 43 | Option1 string `json:"option1,omitempty"` 44 | Option2 string `json:"option2,omitempty"` 45 | Option3 string `json:"option3,omitempty"` 46 | CreatedAt *time.Time `json:"created_at,omitempty"` 47 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 48 | Taxable bool `json:"taxable,omitempty"` 49 | Barcode string `json:"barcode,omitempty"` 50 | ImageID int `json:"image_id,omitempty"` 51 | InventoryQuantity int `json:"inventory_quantity,omitempty"` 52 | Weight *decimal.Decimal `json:"weight,omitempty"` 53 | WeightUnit string `json:"weight_unit,omitempty"` 54 | OldInventoryQuantity int `json:"old_inventory_quantity,omitempty"` 55 | RequireShipping bool `json:"requires_shipping,omitempty"` 56 | } 57 | 58 | // VariantResource represents the result from the variants/X.json endpoint 59 | type VariantResource struct { 60 | Variant *Variant `json:"variant"` 61 | } 62 | 63 | // VariantsResource represents the result from the products/X/variants.json endpoint 64 | type VariantsResource struct { 65 | Variants []Variant `json:"variants"` 66 | } 67 | 68 | // List variants 69 | func (s *VariantServiceOp) List(productID int, options interface{}) ([]Variant, error) { 70 | path := fmt.Sprintf("%s/%d/variants.json", productsBasePath, productID) 71 | resource := new(VariantsResource) 72 | err := s.client.Get(path, resource, options) 73 | return resource.Variants, err 74 | } 75 | 76 | // Count variants 77 | func (s *VariantServiceOp) Count(productID int, options interface{}) (int, error) { 78 | path := fmt.Sprintf("%s/%d/variants/count.json", productsBasePath, productID) 79 | return s.client.Count(path, options) 80 | } 81 | 82 | // Get individual variant 83 | func (s *VariantServiceOp) Get(variantID int, options interface{}) (*Variant, error) { 84 | path := fmt.Sprintf("%s/%d.json", variantsBasePath, variantID) 85 | resource := new(VariantResource) 86 | err := s.client.Get(path, resource, options) 87 | return resource.Variant, err 88 | } 89 | 90 | // Create a new variant 91 | func (s *VariantServiceOp) Create(productID int, variant Variant) (*Variant, error) { 92 | path := fmt.Sprintf("%s/%d/variants.json", productsBasePath, productID) 93 | wrappedData := VariantResource{Variant: &variant} 94 | resource := new(VariantResource) 95 | err := s.client.Post(path, wrappedData, resource) 96 | return resource.Variant, err 97 | } 98 | 99 | // Update existing variant 100 | func (s *VariantServiceOp) Update(variant Variant) (*Variant, error) { 101 | path := fmt.Sprintf("%s/%d.json", variantsBasePath, variant.ID) 102 | wrappedData := VariantResource{Variant: &variant} 103 | resource := new(VariantResource) 104 | err := s.client.Put(path, wrappedData, resource) 105 | return resource.Variant, err 106 | } 107 | 108 | // Delete an existing product 109 | func (s *VariantServiceOp) Delete(productID int, variantID int) error { 110 | return s.client.Delete(fmt.Sprintf("%s/%d/variants/%d.json", productsBasePath, productID, variantID)) 111 | } 112 | -------------------------------------------------------------------------------- /variant_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "github.com/shopspring/decimal" 9 | httpmock "gopkg.in/jarcoal/httpmock.v1" 10 | ) 11 | 12 | func variantTests(t *testing.T, variant Variant) { 13 | // Check that the ID is assigned to the returned variant 14 | expectedInt := 1 15 | if variant.ID != expectedInt { 16 | t.Errorf("Variant.ID returned %+v, expected %+v", variant.ID, expectedInt) 17 | } 18 | 19 | // Check that the Title is assigned to the returned variant 20 | expectedTitle := "Yellow" 21 | if variant.Title != expectedTitle { 22 | t.Errorf("Variant.Title returned %+v, expected %+v", variant.Title, expectedTitle) 23 | } 24 | } 25 | 26 | func TestVariantList(t *testing.T) { 27 | setup() 28 | defer teardown() 29 | 30 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/variants.json", 31 | httpmock.NewStringResponder(200, `{"variants": [{"id":1},{"id":2}]}`)) 32 | 33 | variants, err := client.Variant.List(1, nil) 34 | if err != nil { 35 | t.Errorf("Variant.List returned error: %v", err) 36 | } 37 | 38 | expected := []Variant{{ID: 1}, {ID: 2}} 39 | if !reflect.DeepEqual(variants, expected) { 40 | t.Errorf("Variant.List returned %+v, expected %+v", variants, expected) 41 | } 42 | } 43 | 44 | func TestVariantCount(t *testing.T) { 45 | setup() 46 | defer teardown() 47 | 48 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/variants/count.json", 49 | httpmock.NewStringResponder(200, `{"count": 3}`)) 50 | 51 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/products/1/variants/count.json?created_at_min=2016-01-01T00%3A00%3A00Z", 52 | httpmock.NewStringResponder(200, `{"count": 2}`)) 53 | 54 | cnt, err := client.Variant.Count(1, nil) 55 | if err != nil { 56 | t.Errorf("Variant.Count returned error: %v", err) 57 | } 58 | 59 | expected := 3 60 | if cnt != expected { 61 | t.Errorf("Variant.Count returned %d, expected %d", cnt, expected) 62 | } 63 | 64 | date := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) 65 | cnt, err = client.Variant.Count(1, CountOptions{CreatedAtMin: date}) 66 | if err != nil { 67 | t.Errorf("Variant.Count returned %d, expected %d", cnt, expected) 68 | } 69 | 70 | expected = 2 71 | if cnt != expected { 72 | t.Errorf("Variant.Count returned %d, expected %d", cnt, expected) 73 | } 74 | } 75 | 76 | func TestVariantGet(t *testing.T) { 77 | setup() 78 | defer teardown() 79 | 80 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/variants/1.json", 81 | httpmock.NewStringResponder(200, `{"variant": {"id":1}}`)) 82 | 83 | variant, err := client.Variant.Get(1, nil) 84 | if err != nil { 85 | t.Errorf("Variant.Get returned error: %v", err) 86 | } 87 | 88 | expected := &Variant{ID: 1} 89 | if !reflect.DeepEqual(variant, expected) { 90 | t.Errorf("Variant.Get returned %+v, expected %+v", variant, expected) 91 | } 92 | } 93 | 94 | func TestVariantCreate(t *testing.T) { 95 | setup() 96 | defer teardown() 97 | 98 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/products/1/variants.json", 99 | httpmock.NewBytesResponder(200, loadFixture("variant.json"))) 100 | 101 | price := decimal.NewFromFloat(1) 102 | 103 | variant := Variant{ 104 | Option1: "Yellow", 105 | Price: &price, 106 | } 107 | result, err := client.Variant.Create(1, variant) 108 | if err != nil { 109 | t.Errorf("Variant.Create returned error: %v", err) 110 | } 111 | variantTests(t, *result) 112 | } 113 | 114 | func TestVariantUpdate(t *testing.T) { 115 | setup() 116 | defer teardown() 117 | 118 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/variants/1.json", 119 | httpmock.NewBytesResponder(200, loadFixture("variant.json"))) 120 | 121 | variant := Variant{ 122 | ID: 1, 123 | Option1: "Green", 124 | } 125 | 126 | variant.Option1 = "Yellow" 127 | 128 | returnedVariant, err := client.Variant.Update(variant) 129 | if err != nil { 130 | t.Errorf("Variant.Update returned error: %v", err) 131 | } 132 | variantTests(t, *returnedVariant) 133 | } 134 | 135 | func TestVariantDelete(t *testing.T) { 136 | setup() 137 | defer teardown() 138 | 139 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/products/1/variants/1.json", 140 | httpmock.NewStringResponder(200, "{}")) 141 | 142 | err := client.Variant.Delete(1, 1) 143 | if err != nil { 144 | t.Errorf("Variant.Delete returned error: %v", err) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /webhook.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const webhooksBasePath = "admin/webhooks" 9 | 10 | // WebhookService is an interface for interfacing with the webhook endpoints of 11 | // the Shopify API. 12 | // See: https://help.shopify.com/api/reference/webhook 13 | type WebhookService interface { 14 | List(interface{}) ([]Webhook, error) 15 | Count(interface{}) (int, error) 16 | Get(int, interface{}) (*Webhook, error) 17 | Create(Webhook) (*Webhook, error) 18 | Update(Webhook) (*Webhook, error) 19 | Delete(int) error 20 | } 21 | 22 | // WebhookServiceOp handles communication with the webhook-related methods of 23 | // the Shopify API. 24 | type WebhookServiceOp struct { 25 | client *Client 26 | } 27 | 28 | // Webhook represents a Shopify webhook 29 | type Webhook struct { 30 | ID int `json:"id"` 31 | Address string `json:"address"` 32 | Topic string `json:"topic"` 33 | Format string `json:"format"` 34 | CreatedAt *time.Time `json:"created_at,omitempty"` 35 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 36 | Fields []string `json:"fields"` 37 | MetafieldNamespaces []string `json:"metafield_namespaces"` 38 | } 39 | 40 | // WebhookOptions can be used for filtering webhooks on a List request. 41 | type WebhookOptions struct { 42 | Address string `url:"address,omitempty"` 43 | Topic string `url:"topic,omitempty"` 44 | } 45 | 46 | // WebhookResource represents the result from the admin/webhooks.json endpoint 47 | type WebhookResource struct { 48 | Webhook *Webhook `json:"webhook"` 49 | } 50 | 51 | // WebhooksResource is the root object for a webhook get request. 52 | type WebhooksResource struct { 53 | Webhooks []Webhook `json:"webhooks"` 54 | } 55 | 56 | // List webhooks 57 | func (s *WebhookServiceOp) List(options interface{}) ([]Webhook, error) { 58 | path := fmt.Sprintf("%s.json", webhooksBasePath) 59 | resource := new(WebhooksResource) 60 | err := s.client.Get(path, resource, options) 61 | return resource.Webhooks, err 62 | } 63 | 64 | // Count webhooks 65 | func (s *WebhookServiceOp) Count(options interface{}) (int, error) { 66 | path := fmt.Sprintf("%s/count.json", webhooksBasePath) 67 | return s.client.Count(path, options) 68 | } 69 | 70 | // Get individual webhook 71 | func (s *WebhookServiceOp) Get(webhookdID int, options interface{}) (*Webhook, error) { 72 | path := fmt.Sprintf("%s/%d.json", webhooksBasePath, webhookdID) 73 | resource := new(WebhookResource) 74 | err := s.client.Get(path, resource, options) 75 | return resource.Webhook, err 76 | } 77 | 78 | // Create a new webhook 79 | func (s *WebhookServiceOp) Create(webhook Webhook) (*Webhook, error) { 80 | path := fmt.Sprintf("%s.json", webhooksBasePath) 81 | wrappedData := WebhookResource{Webhook: &webhook} 82 | resource := new(WebhookResource) 83 | err := s.client.Post(path, wrappedData, resource) 84 | return resource.Webhook, err 85 | } 86 | 87 | // Update an existing webhook. 88 | func (s *WebhookServiceOp) Update(webhook Webhook) (*Webhook, error) { 89 | path := fmt.Sprintf("%s/%d.json", webhooksBasePath, webhook.ID) 90 | wrappedData := WebhookResource{Webhook: &webhook} 91 | resource := new(WebhookResource) 92 | err := s.client.Put(path, wrappedData, resource) 93 | return resource.Webhook, err 94 | } 95 | 96 | // Delete an existing webhooks 97 | func (s *WebhookServiceOp) Delete(ID int) error { 98 | return s.client.Delete(fmt.Sprintf("%s/%d.json", webhooksBasePath, ID)) 99 | } 100 | -------------------------------------------------------------------------------- /webhook_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "gopkg.in/jarcoal/httpmock.v1" 9 | ) 10 | 11 | func webhookTests(t *testing.T, webhook Webhook) { 12 | // Check that dates are parsed 13 | d := time.Date(2016, time.June, 1, 14, 10, 44, 0, time.UTC) 14 | if !d.Equal(*webhook.CreatedAt) { 15 | t.Errorf("Webhook.CreatedAt returned %+v, expected %+v", webhook.CreatedAt, d) 16 | } 17 | 18 | expectedStr := "http://apple.com" 19 | if webhook.Address != expectedStr { 20 | t.Errorf("Webhook.Address returned %+v, expected %+v", webhook.Address, expectedStr) 21 | } 22 | 23 | expectedStr = "orders/create" 24 | if webhook.Topic != expectedStr { 25 | t.Errorf("Webhook.Topic returned %+v, expected %+v", webhook.Topic, expectedStr) 26 | } 27 | 28 | expectedArr := []string{"id", "updated_at"} 29 | if !reflect.DeepEqual(webhook.Fields, expectedArr) { 30 | t.Errorf("Webhook.Fields returned %+v, expected %+v", webhook.Fields, expectedArr) 31 | } 32 | 33 | expectedArr = []string{"google", "inventory"} 34 | if !reflect.DeepEqual(webhook.MetafieldNamespaces, expectedArr) { 35 | t.Errorf("Webhook.Fields returned %+v, expected %+v", webhook.MetafieldNamespaces, expectedArr) 36 | } 37 | } 38 | 39 | func TestWebhookList(t *testing.T) { 40 | setup() 41 | defer teardown() 42 | 43 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/webhooks.json", 44 | httpmock.NewBytesResponder(200, loadFixture("webhooks.json"))) 45 | 46 | webhooks, err := client.Webhook.List(nil) 47 | if err != nil { 48 | t.Errorf("Webhook.List returned error: %v", err) 49 | } 50 | 51 | // Check that webhooks were parsed 52 | if len(webhooks) != 1 { 53 | t.Errorf("Webhook.List got %v webhooks, expected: 1", len(webhooks)) 54 | } 55 | 56 | webhookTests(t, webhooks[0]) 57 | } 58 | 59 | func TestWebhookGet(t *testing.T) { 60 | setup() 61 | defer teardown() 62 | 63 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/webhooks/4759306.json", 64 | httpmock.NewBytesResponder(200, loadFixture("webhook.json"))) 65 | 66 | webhook, err := client.Webhook.Get(4759306, nil) 67 | if err != nil { 68 | t.Errorf("Webhook.Get returned error: %v", err) 69 | } 70 | 71 | webhookTests(t, *webhook) 72 | } 73 | 74 | func TestWebhookCount(t *testing.T) { 75 | setup() 76 | defer teardown() 77 | 78 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/webhooks/count.json", 79 | httpmock.NewStringResponder(200, `{"count": 7}`)) 80 | 81 | httpmock.RegisterResponder("GET", "https://fooshop.myshopify.com/admin/webhooks/count.json?topic=orders%2Fpaid", 82 | httpmock.NewStringResponder(200, `{"count": 2}`)) 83 | 84 | cnt, err := client.Webhook.Count(nil) 85 | if err != nil { 86 | t.Errorf("Webhook.Count returned error: %v", err) 87 | } 88 | 89 | expected := 7 90 | if cnt != expected { 91 | t.Errorf("Webhook.Count returned %d, expected %d", cnt, expected) 92 | } 93 | 94 | options := WebhookOptions{Topic: "orders/paid"} 95 | cnt, err = client.Webhook.Count(options) 96 | if err != nil { 97 | t.Errorf("Webhook.Count returned error: %v", err) 98 | } 99 | 100 | expected = 2 101 | if cnt != expected { 102 | t.Errorf("Webhook.Count returned %d, expected %d", cnt, expected) 103 | } 104 | } 105 | 106 | func TestWebhookCreate(t *testing.T) { 107 | setup() 108 | defer teardown() 109 | 110 | httpmock.RegisterResponder("POST", "https://fooshop.myshopify.com/admin/webhooks.json", 111 | httpmock.NewBytesResponder(200, loadFixture("webhook.json"))) 112 | 113 | webhook := Webhook{ 114 | Topic: "orders/create", 115 | Address: "http://example.com", 116 | } 117 | 118 | returnedWebhook, err := client.Webhook.Create(webhook) 119 | if err != nil { 120 | t.Errorf("Webhook.Create returned error: %v", err) 121 | } 122 | 123 | webhookTests(t, *returnedWebhook) 124 | } 125 | 126 | func TestWebhookUpdate(t *testing.T) { 127 | setup() 128 | defer teardown() 129 | 130 | httpmock.RegisterResponder("PUT", "https://fooshop.myshopify.com/admin/webhooks/4759306.json", 131 | httpmock.NewBytesResponder(200, loadFixture("webhook.json"))) 132 | 133 | webhook := Webhook{ 134 | ID: 4759306, 135 | Topic: "orders/create", 136 | Address: "http://example.com", 137 | } 138 | 139 | returnedWebhook, err := client.Webhook.Update(webhook) 140 | if err != nil { 141 | t.Errorf("Webhook.Update returned error: %v", err) 142 | } 143 | 144 | webhookTests(t, *returnedWebhook) 145 | } 146 | 147 | func TestWebhookDelete(t *testing.T) { 148 | setup() 149 | defer teardown() 150 | 151 | httpmock.RegisterResponder("DELETE", "https://fooshop.myshopify.com/admin/webhooks/4759306.json", 152 | httpmock.NewStringResponder(200, "{}")) 153 | 154 | err := client.Webhook.Delete(4759306) 155 | if err != nil { 156 | t.Errorf("Webhook.Delete returned error: %v", err) 157 | } 158 | } 159 | --------------------------------------------------------------------------------