├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── abandoned_checkout.go ├── abandoned_checkout_test.go ├── access_scopes.go ├── access_scopes_test.go ├── api_permissions.go ├── api_permissions_test.go ├── applicationcharge.go ├── applicationcharge_test.go ├── articles.go ├── articles_test.go ├── asset.go ├── asset_test.go ├── assigned_fulfillment_order.go ├── assigned_fulfillment_order_test.go ├── blog.go ├── blog_test.go ├── carrier.go ├── carrier_test.go ├── collect.go ├── collect_test.go ├── collection.go ├── collection_test.go ├── customcollection.go ├── customcollection_test.go ├── customer.go ├── customer_address.go ├── customer_address_test.go ├── customer_test.go ├── discount_code.go ├── discount_code_test.go ├── docker-compose.yml ├── draft_order.go ├── draft_order_test.go ├── fixtures ├── access_scopes.json ├── applicationcharge.json ├── asset.json ├── blog.json ├── carrier_service.json ├── carrier_services.json ├── collect.json ├── customcollection.json ├── customer.json ├── customer_address.json ├── customer_addresses.json ├── customer_tags.json ├── discount_code.json ├── draft_order.json ├── draft_orders.json ├── fulfillment.json ├── fulfillment_accept.json ├── fulfillment_event.json ├── fulfillment_events.json ├── fulfillment_order.json ├── fulfillment_order_move.json ├── fulfillment_reject.json ├── fulfillment_send.json ├── fulfillment_service.json ├── fulfillment_services.json ├── gift_card │ ├── get.json │ └── list.json ├── image.json ├── images.json ├── inventory_item.json ├── inventory_items.json ├── inventory_level.json ├── inventory_levels.json ├── invoice.json ├── location.json ├── locations.json ├── metafield.json ├── order.json ├── order_risk.json ├── order_risks.json ├── order_with_transaction.json ├── orderlineitems │ ├── properties_empty_object.json │ ├── properties_invalid0.json │ ├── properties_invalid1.json │ ├── properties_invalid2.json │ ├── properties_object.json │ ├── properties_ommited.json │ └── valid.json ├── orders.json ├── page.json ├── payments_transaction.json ├── payments_transactions.json ├── payout.json ├── payouts.json ├── payouts_filtered.json ├── price_rule │ ├── get.json │ └── list.json ├── product.json ├── product_listing.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 ├── shipping_zones.json ├── shippinglines │ ├── requested_fulfillment_service_id_invalid.json │ ├── requested_fulfillment_service_id_null.json │ ├── requested_fulfillment_service_id_number.json │ └── valid.json ├── shop.json ├── smartcollection.json ├── storefront_access_token.json ├── storefront_access_tokens.json ├── theme.json ├── transaction.json ├── transactions.json ├── usagecharge.json ├── usagecharges.json ├── variant.json ├── variant_with_metafields.json ├── variant_with_taxcode.json ├── webhook.json └── webhooks.json ├── fulfillment.go ├── fulfillment_event.go ├── fulfillment_event_test.go ├── fulfillment_order.go ├── fulfillment_order_test.go ├── fulfillment_request.go ├── fulfillment_request_test.go ├── fulfillment_service.go ├── fulfillment_service_test.go ├── fulfillment_test.go ├── gift_card.go ├── gift_card_test.go ├── go.mod ├── go.sum ├── goshopify.go ├── goshopify_test.go ├── graphql.go ├── graphql_test.go ├── image.go ├── image_test.go ├── inventory_item.go ├── inventory_item_test.go ├── inventory_level.go ├── inventory_level_test.go ├── location.go ├── location_test.go ├── logger.go ├── logger_test.go ├── make.bat ├── metafield.go ├── metafield_test.go ├── oauth.go ├── oauth_test.go ├── options.go ├── options_test.go ├── order.go ├── order_risk.go ├── order_risk_test.go ├── order_test.go ├── page.go ├── page_test.go ├── payments_transactions.go ├── payments_transactions_test.go ├── payouts.go ├── payouts_test.go ├── price_rule.go ├── price_rule_test.go ├── product.go ├── product_listing.go ├── product_listing_test.go ├── product_test.go ├── recurringapplicationcharge.go ├── recurringapplicationcharge_test.go ├── redirect.go ├── redirect_test.go ├── scripttag.go ├── scripttag_test.go ├── shipping_zone.go ├── shipping_zone_test.go ├── shop.go ├── shop_test.go ├── smartcollection.go ├── smartcollection_test.go ├── storefrontaccesstoken.go ├── storefrontaccesstoken_test.go ├── theme.go ├── theme_test.go ├── transaction.go ├── transaction_test.go ├── usagecharge.go ├── usagecharge_test.go ├── util.go ├── util_test.go ├── variant.go ├── variant_test.go ├── webhook.go └── webhook_test.go /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.21', '1.22', '1.23' ] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Setup Go ${{ matrix.go-version }} 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -coverprofile=coverage.txt -v ./... 26 | 27 | - name: Upload code coverage results 28 | uses: codecov/codecov-action@v3 29 | with: 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | fail_ci_if_error: false 32 | verbose: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | coverage.* 3 | .vscode 4 | .idea 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | dist: bionic 3 | go: 4 | - "1.10" 5 | - "1.11" 6 | - "1.12" 7 | - "1.13" 8 | - "1.14" 9 | - "1.15" 10 | install: 11 | - make travis 12 | script: 13 | - go test -coverprofile=coverage.txt 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine 2 | 3 | ENV CGO_ENABLED=0 4 | RUN mkdir -p /go/src/github.com/bold-commerce/go-shopify 5 | WORKDIR /go/src/github.com/bold-commerce/go-shopify 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | container: 2 | @docker-compose build test 3 | travis: 4 | @go get -v -d -t 5 | test: 6 | @docker-compose run --rm test 7 | coverage: 8 | @docker-compose run --rm test sh -c 'go test -coverprofile=coverage.out ./... && go tool cover -html coverage.out -o coverage.html' 9 | @open coverage.html 10 | clean: 11 | @docker image rm go-shopify 12 | @rm -f coverage.html coverage.out 13 | 14 | .DEFAULT_GOAL := container 15 | -------------------------------------------------------------------------------- /abandoned_checkout_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/jarcoal/httpmock" 10 | ) 11 | 12 | func TestAbandonedCheckoutList(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | httpmock.RegisterResponder( 17 | "GET", 18 | fmt.Sprintf("https://fooshop.myshopify.com/%s/checkouts.json", client.pathPrefix), 19 | httpmock.NewStringResponder( 20 | 200, 21 | `{"checkouts": [{"id":1},{"id":2}]}`, 22 | ), 23 | ) 24 | 25 | abandonedCheckouts, err := client.AbandonedCheckout.List(context.Background(), nil) 26 | if err != nil { 27 | t.Errorf("AbandonedCheckout.List returned error: %v", err) 28 | } 29 | 30 | expected := []AbandonedCheckout{{Id: 1}, {Id: 2}} 31 | if !reflect.DeepEqual(abandonedCheckouts, expected) { 32 | t.Errorf("AbandonedCheckout.List returned %+v, expected %+v", abandonedCheckouts, expected) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /access_scopes.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type AccessScopesService interface { 8 | List(context.Context, interface{}) ([]AccessScope, error) 9 | } 10 | 11 | type AccessScope struct { 12 | Handle string `json:"handle,omitempty"` 13 | } 14 | 15 | // AccessScopesResource represents the result from the oauth/access_scopes.json endpoint 16 | type AccessScopesResource struct { 17 | AccessScopes []AccessScope `json:"access_scopes,omitempty"` 18 | } 19 | 20 | // AccessScopesServiceOp handles communication with the Access Scopes 21 | // related methods of the Shopify API 22 | type AccessScopesServiceOp struct { 23 | client *Client 24 | } 25 | 26 | // List gets access scopes based on used oauth token 27 | func (s *AccessScopesServiceOp) List(ctx context.Context, options interface{}) ([]AccessScope, error) { 28 | path := "admin/oauth/access_scopes.json" 29 | resource := new(AccessScopesResource) 30 | req, err := s.client.NewRequest(ctx, "GET", path, nil, options) 31 | if err != nil { 32 | return nil, err 33 | } 34 | err = s.client.Do(req, resource) 35 | return resource.AccessScopes, err 36 | } 37 | -------------------------------------------------------------------------------- /access_scopes_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/jarcoal/httpmock" 9 | ) 10 | 11 | func TestAccessScopesServiceOp_List(t *testing.T) { 12 | setup() 13 | defer teardown() 14 | 15 | httpmock.RegisterResponder( 16 | "GET", 17 | "https://fooshop.myshopify.com/admin/oauth/access_scopes.json", 18 | httpmock.NewBytesResponder(200, loadFixture("access_scopes.json")), 19 | ) 20 | 21 | scopeResponse, err := client.AccessScopes.List(context.Background(), nil) 22 | if err != nil { 23 | t.Errorf("AccessScopes.List returned an error: %v", err) 24 | } 25 | 26 | expected := []AccessScope{ 27 | { 28 | Handle: "scope_1", 29 | }, 30 | { 31 | Handle: "scope_2", 32 | }, 33 | } 34 | if !reflect.DeepEqual(scopeResponse, expected) { 35 | t.Errorf("AccessScopes.List returned %+v, expected %+v", expected, expected) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /api_permissions.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | const apiPermissionsBasePath = "api_permissions" 9 | 10 | // ApiPermissionsService is an interface for interfacing with the API 11 | // permissions endpoints of the Shopify API. 12 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/theme 13 | type ApiPermissionsService interface { 14 | Delete(context.Context) error 15 | } 16 | 17 | // ApiPermissionsServiceOp handles communication with the theme related methods of 18 | // the Shopify API. 19 | type ApiPermissionsServiceOp struct { 20 | client *Client 21 | } 22 | 23 | // Uninstall an app. 24 | func (s *ApiPermissionsServiceOp) Delete(ctx context.Context) error { 25 | path := fmt.Sprintf("%s/current.json", apiPermissionsBasePath) 26 | return s.client.Delete(ctx, path) 27 | } 28 | -------------------------------------------------------------------------------- /api_permissions_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/jarcoal/httpmock" 9 | ) 10 | 11 | func TestApiPermissionsDelete(t *testing.T) { 12 | setup() 13 | defer teardown() 14 | 15 | httpmock.RegisterResponder("DELETE", 16 | fmt.Sprintf("https://fooshop.myshopify.com/%s/%s/current.json", client.pathPrefix, apiPermissionsBasePath), 17 | httpmock.NewStringResponder(200, "")) 18 | 19 | err := client.ApiPermissions.Delete(context.Background()) 20 | if err != nil { 21 | t.Errorf("Theme.Delete returned error: %v", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /applicationcharge.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | const applicationChargesBasePath = "application_charges" 12 | 13 | // ApplicationChargeService is an interface for interacting with the 14 | // ApplicationCharge endpoints of the Shopify API. 15 | // See https://shopify.dev/docs/api/admin-rest/latest/resources/applicationcharge 16 | type ApplicationChargeService interface { 17 | Create(context.Context, ApplicationCharge) (*ApplicationCharge, error) 18 | Get(context.Context, uint64, interface{}) (*ApplicationCharge, error) 19 | List(context.Context, interface{}) ([]ApplicationCharge, error) 20 | Activate(context.Context, ApplicationCharge) (*ApplicationCharge, error) 21 | } 22 | 23 | type ApplicationChargeServiceOp struct { 24 | client *Client 25 | } 26 | 27 | type ApplicationCharge struct { 28 | Id uint64 `json:"id"` 29 | Name string `json:"name"` 30 | APIClientId uint64 `json:"api_client_id"` 31 | Price *decimal.Decimal `json:"price"` 32 | Status string `json:"status"` 33 | ReturnURL string `json:"return_url"` 34 | Test *bool `json:"test"` 35 | CreatedAt *time.Time `json:"created_at"` 36 | UpdatedAt *time.Time `json:"updated_at"` 37 | ChargeType *string `json:"charge_type"` 38 | DecoratedReturnURL string `json:"decorated_return_url"` 39 | ConfirmationURL string `json:"confirmation_url"` 40 | } 41 | 42 | // ApplicationChargeResource represents the result from the 43 | // admin/application_charges{/X{/activate.json}.json}.json endpoints. 44 | type ApplicationChargeResource struct { 45 | Charge *ApplicationCharge `json:"application_charge"` 46 | } 47 | 48 | // ApplicationChargesResource represents the result from the 49 | // admin/application_charges.json endpoint. 50 | type ApplicationChargesResource struct { 51 | Charges []ApplicationCharge `json:"application_charges"` 52 | } 53 | 54 | // Create creates new application charge. 55 | func (a ApplicationChargeServiceOp) Create(ctx context.Context, charge ApplicationCharge) (*ApplicationCharge, error) { 56 | path := fmt.Sprintf("%s.json", applicationChargesBasePath) 57 | resource := &ApplicationChargeResource{} 58 | return resource.Charge, a.client.Post(ctx, path, ApplicationChargeResource{Charge: &charge}, resource) 59 | } 60 | 61 | // Get gets individual application charge. 62 | func (a ApplicationChargeServiceOp) Get(ctx context.Context, chargeId uint64, options interface{}) (*ApplicationCharge, error) { 63 | path := fmt.Sprintf("%s/%d.json", applicationChargesBasePath, chargeId) 64 | resource := &ApplicationChargeResource{} 65 | return resource.Charge, a.client.Get(ctx, path, resource, options) 66 | } 67 | 68 | // List gets all application charges. 69 | func (a ApplicationChargeServiceOp) List(ctx context.Context, options interface{}) ([]ApplicationCharge, error) { 70 | path := fmt.Sprintf("%s.json", applicationChargesBasePath) 71 | resource := &ApplicationChargesResource{} 72 | return resource.Charges, a.client.Get(ctx, path, resource, options) 73 | } 74 | 75 | // Activate activates application charge. 76 | func (a ApplicationChargeServiceOp) Activate(ctx context.Context, charge ApplicationCharge) (*ApplicationCharge, error) { 77 | path := fmt.Sprintf("%s/%d/activate.json", applicationChargesBasePath, charge.Id) 78 | resource := &ApplicationChargeResource{} 79 | return resource.Charge, a.client.Post(ctx, path, ApplicationChargeResource{Charge: &charge}, resource) 80 | } 81 | -------------------------------------------------------------------------------- /asset.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const assetsBasePath = "themes" 10 | 11 | // AssetService is an interface for interfacing with the asset endpoints 12 | // of the Shopify API. 13 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/asset 14 | type AssetService interface { 15 | List(context.Context, uint64, interface{}) ([]Asset, error) 16 | Get(context.Context, uint64, string) (*Asset, error) 17 | Update(context.Context, uint64, Asset) (*Asset, error) 18 | Delete(context.Context, uint64, string) error 19 | } 20 | 21 | // AssetServiceOp handles communication with the asset related methods of 22 | // the Shopify API. 23 | type AssetServiceOp struct { 24 | client *Client 25 | } 26 | 27 | // Asset represents a Shopify asset 28 | type Asset struct { 29 | Attachment string `json:"attachment,omitempty"` 30 | ContentType string `json:"content_type,omitempty"` 31 | Key string `json:"key,omitempty"` 32 | PublicURL string `json:"public_url,omitempty"` 33 | Size int `json:"size,omitempty"` 34 | SourceKey string `json:"source_key,omitempty"` 35 | Src string `json:"src,omitempty"` 36 | ThemeId uint64 `json:"theme_id,omitempty"` 37 | Value string `json:"value,omitempty"` 38 | CreatedAt *time.Time `json:"created_at,omitempty"` 39 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 40 | } 41 | 42 | // AssetResource is the result from the themes/x/assets.json?asset[key]= endpoint 43 | type AssetResource struct { 44 | Asset *Asset `json:"asset"` 45 | } 46 | 47 | // AssetsResource is the result from the themes/x/assets.json endpoint 48 | type AssetsResource struct { 49 | Assets []Asset `json:"assets"` 50 | } 51 | 52 | type assetGetOptions struct { 53 | Key string `url:"asset[key]"` 54 | ThemeId uint64 `url:"theme_id"` 55 | } 56 | 57 | // List the metadata for all assets in the given theme 58 | func (s *AssetServiceOp) List(ctx context.Context, themeId uint64, options interface{}) ([]Asset, error) { 59 | path := fmt.Sprintf("%s/%d/assets.json", assetsBasePath, themeId) 60 | resource := new(AssetsResource) 61 | err := s.client.Get(ctx, path, resource, options) 62 | return resource.Assets, err 63 | } 64 | 65 | // Get an asset by key from the given theme 66 | func (s *AssetServiceOp) Get(ctx context.Context, themeId uint64, key string) (*Asset, error) { 67 | path := fmt.Sprintf("%s/%d/assets.json", assetsBasePath, themeId) 68 | options := assetGetOptions{ 69 | Key: key, 70 | ThemeId: themeId, 71 | } 72 | resource := new(AssetResource) 73 | err := s.client.Get(ctx, path, resource, options) 74 | return resource.Asset, err 75 | } 76 | 77 | // Update an asset 78 | func (s *AssetServiceOp) Update(ctx context.Context, themeId uint64, asset Asset) (*Asset, error) { 79 | path := fmt.Sprintf("%s/%d/assets.json", assetsBasePath, themeId) 80 | wrappedData := AssetResource{Asset: &asset} 81 | resource := new(AssetResource) 82 | err := s.client.Put(ctx, path, wrappedData, resource) 83 | return resource.Asset, err 84 | } 85 | 86 | // Delete an asset 87 | func (s *AssetServiceOp) Delete(ctx context.Context, themeId uint64, key string) error { 88 | path := fmt.Sprintf("%s/%d/assets.json?asset[key]=%s", assetsBasePath, themeId, key) 89 | return s.client.Delete(ctx, path) 90 | } 91 | -------------------------------------------------------------------------------- /asset_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/jarcoal/httpmock" 10 | ) 11 | 12 | func TestAssetList(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | httpmock.RegisterResponder( 17 | "GET", 18 | fmt.Sprintf("https://fooshop.myshopify.com/%s/themes/1/assets.json", client.pathPrefix), 19 | httpmock.NewStringResponder( 20 | 200, 21 | `{"assets": [{"key":"assets\/1.liquid"},{"key":"assets\/2.liquid"}]}`, 22 | ), 23 | ) 24 | 25 | assets, err := client.Asset.List(context.Background(), 1, nil) 26 | if err != nil { 27 | t.Errorf("Asset.List returned error: %v", err) 28 | } 29 | 30 | expected := []Asset{{Key: "assets/1.liquid"}, {Key: "assets/2.liquid"}} 31 | if !reflect.DeepEqual(assets, expected) { 32 | t.Errorf("Asset.List returned %+v, expected %+v", assets, expected) 33 | } 34 | } 35 | 36 | func TestAssetGet(t *testing.T) { 37 | setup() 38 | defer teardown() 39 | params := map[string]string{ 40 | "asset[key]": "foo/bar.liquid", 41 | "theme_id": "1", 42 | } 43 | httpmock.RegisterResponderWithQuery( 44 | "GET", 45 | fmt.Sprintf("https://fooshop.myshopify.com/%s/themes/1/assets.json", client.pathPrefix), 46 | params, 47 | httpmock.NewStringResponder( 48 | 200, 49 | `{"asset": {"key":"foo\/bar.liquid"}}`, 50 | ), 51 | ) 52 | 53 | asset, err := client.Asset.Get(context.Background(), 1, "foo/bar.liquid") 54 | if err != nil { 55 | t.Errorf("Asset.Get returned error: %v", err) 56 | } 57 | 58 | expected := &Asset{Key: "foo/bar.liquid"} 59 | if !reflect.DeepEqual(asset, expected) { 60 | t.Errorf("Asset.Get returned %+v, expected %+v", asset, expected) 61 | } 62 | } 63 | 64 | func TestAssetUpdate(t *testing.T) { 65 | setup() 66 | defer teardown() 67 | 68 | httpmock.RegisterResponder( 69 | "PUT", 70 | fmt.Sprintf("https://fooshop.myshopify.com/%s/themes/1/assets.json", client.pathPrefix), 71 | httpmock.NewBytesResponder( 72 | 200, 73 | loadFixture("asset.json"), 74 | ), 75 | ) 76 | 77 | asset := Asset{ 78 | Key: "templates/index.liquid", 79 | Value: "content", 80 | } 81 | 82 | returnedAsset, err := client.Asset.Update(context.Background(), 1, asset) 83 | if err != nil { 84 | t.Errorf("Asset.Update returned error: %v", err) 85 | } 86 | if returnedAsset == nil { 87 | t.Errorf("Asset.Update returned nil") 88 | } 89 | } 90 | 91 | func TestAssetDelete(t *testing.T) { 92 | setup() 93 | defer teardown() 94 | 95 | params := map[string]string{"asset[key]": "foo/bar.liquid"} 96 | httpmock.RegisterResponderWithQuery( 97 | "DELETE", 98 | fmt.Sprintf("https://fooshop.myshopify.com/%s/themes/1/assets.json", client.pathPrefix), 99 | params, 100 | httpmock.NewStringResponder(200, "{}"), 101 | ) 102 | 103 | err := client.Asset.Delete(context.Background(), 1, "foo/bar.liquid") 104 | if err != nil { 105 | t.Errorf("Asset.Delete returned error: %v", err) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /assigned_fulfillment_order.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | assignedFulfillmentOrderBasePath = "assigned_fulfillment_orders" 10 | ) 11 | 12 | // AssignedFulfillmentService is an interface for interfacing with the assigned fulfillment orders 13 | // of the Shopify API. 14 | // https://shopify.dev/docs/api/admin-rest/2023-07/resources/assignedfulfillmentorder 15 | type AssignedFulfillmentOrderService interface { 16 | Get(context.Context, interface{}) ([]AssignedFulfillmentOrder, error) 17 | } 18 | 19 | type AssignedFulfillmentOrder struct { 20 | Id uint64 `json:"id,omitempty"` 21 | AssignedLocationId uint64 `json:"assigned_location_id,omitempty"` 22 | Destination AssignedFulfillmentOrderDestination `json:"destination,omitempty"` 23 | LineItems []AssignedFulfillmentOrderLineItem `json:"line_items,omitempty"` 24 | OrderId uint64 `json:"order_id,omitempty"` 25 | RequestStatus string `json:"request_status,omitempty"` 26 | ShopId uint64 `json:"shop_id,omitempty"` 27 | Status string `json:"status,omitempty"` 28 | } 29 | 30 | // AssignedFulfillmentOrderDestination represents a destination for a AssignedFulfillmentOrder 31 | type AssignedFulfillmentOrderDestination struct { 32 | Id uint64 `json:"id,omitempty"` 33 | Address1 string `json:"address1,omitempty"` 34 | Address2 string `json:"address2,omitempty"` 35 | City string `json:"city,omitempty"` 36 | Company string `json:"company,omitempty"` 37 | Country string `json:"country,omitempty"` 38 | Email string `json:"email,omitempty"` 39 | FirstName string `json:"first_name,omitempty"` 40 | LastName string `json:"last_name,omitempty"` 41 | Phone string `json:"phone,omitempty"` 42 | Province string `json:"province,omitempty"` 43 | Zip string `json:"zip,omitempty"` 44 | } 45 | 46 | // AssignedFulfillmentOrderLineItem represents a line item for a AssignedFulfillmentOrder 47 | type AssignedFulfillmentOrderLineItem struct { 48 | Id uint64 `json:"id,omitempty"` 49 | ShopId uint64 `json:"shop_id,omitempty"` 50 | FulfillmentOrderId uint64 `json:"fulfillment_order_id,omitempty"` 51 | LineItemId uint64 `json:"line_item_id,omitempty"` 52 | InventoryItemId uint64 `json:"inventory_item_id,omitempty"` 53 | Quantity uint64 `json:"quantity,omitempty"` 54 | FulfillableQuantity uint64 `json:"fulfillable_quantity,omitempty"` 55 | VariantId uint64 `json:"variant_id,omitempty"` 56 | } 57 | 58 | // AssignedFulfillmentOrderResource represents the result from the assigned_fulfillment_order.json endpoint 59 | type AssignedFulfillmentOrdersResource struct { 60 | AssignedFulfillmentOrders []AssignedFulfillmentOrder `json:"fulfillment_orders,omitempty"` 61 | } 62 | 63 | type AssignedFulfillmentOrderOptions struct { 64 | AssignmentStatus string `url:"assignment_status,omitempty"` 65 | LocationIds string `url:"location_ids,omitempty"` 66 | } 67 | 68 | // AssignedFulfillmentOrderServiceOp handles communication with the AssignedFulfillmentOrderService 69 | // related methods of the Shopify API 70 | type AssignedFulfillmentOrderServiceOp struct { 71 | client *Client 72 | } 73 | 74 | // Gets a list of all the fulfillment orders that are assigned to an app at the shop level 75 | func (s *AssignedFulfillmentOrderServiceOp) Get(ctx context.Context, options interface{}) ([]AssignedFulfillmentOrder, error) { 76 | path := fmt.Sprintf("%s.json", assignedFulfillmentOrderBasePath) 77 | resource := new(AssignedFulfillmentOrdersResource) 78 | err := s.client.Get(ctx, path, resource, options) 79 | return resource.AssignedFulfillmentOrders, err 80 | } 81 | -------------------------------------------------------------------------------- /assigned_fulfillment_order_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/jarcoal/httpmock" 10 | ) 11 | 12 | func AssignedFulfillmentOrderTests(t *testing.T, assignedFulfillmentOrder AssignedFulfillmentOrder) { 13 | // Check that Id is assigned to the returned fulfillment 14 | expectedInt := uint64(255858046) // in assigned_fulfillment_orders.json fixture 15 | if assignedFulfillmentOrder.Id != expectedInt { 16 | t.Errorf("AssignedFulfillmentOrder.Id returned %+v, expected %+v", assignedFulfillmentOrder.Id, expectedInt) 17 | } 18 | } 19 | 20 | func TestAssignedFulfillmentOrderGet(t *testing.T) { 21 | setup() 22 | defer teardown() 23 | 24 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/assigned_fulfillment_orders.json", client.pathPrefix), 25 | httpmock.NewStringResponder(200, `{"fulfillment_orders": [{"id":1},{"id":2}]}`)) 26 | 27 | assignedFulfillmentOrderService := &AssignedFulfillmentOrderServiceOp{client: client} 28 | 29 | assignedFulfillmentOrders, err := assignedFulfillmentOrderService.Get(context.Background(), nil) 30 | if err != nil { 31 | t.Errorf("AssignedFulfillmentOrder.List returned error: %v", err) 32 | } 33 | 34 | expected := []AssignedFulfillmentOrder{{Id: 1}, {Id: 2}} 35 | if !reflect.DeepEqual(assignedFulfillmentOrders, expected) { 36 | t.Errorf("AssignedFulfillmentOrder.List returned %+v, expected %+v", assignedFulfillmentOrders, expected) 37 | } 38 | } 39 | 40 | // func TestFulfillmentOrderGet(t *testing.T) { 41 | // setup() 42 | // defer teardown() 43 | 44 | // fixture := loadFixture("fulfillment_order.json") 45 | // httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/fulfillment_orders/255858046.json", client.pathPrefix), 46 | // httpmock.NewBytesResponder(200, fixture)) 47 | 48 | // fulfillmentOrderService := &FulfillmentOrderServiceOp{client: client} 49 | 50 | // fulfillment, err := fulfillmentOrderService.Get(context.Background(), 255858046, nil) 51 | // if err != nil { 52 | // t.Errorf("FulfillmentOrder.Get returned error: %v", err) 53 | // } 54 | 55 | // expected := FulfillmentOrderResource{} 56 | // err = json.Unmarshal(fixture, &expected) 57 | // if err != nil { 58 | // t.Errorf("json.Unmarshall returned error : %v", err) 59 | // } 60 | 61 | // if !reflect.DeepEqual(fulfillment, expected.FulfillmentOrder) { 62 | // t.Errorf("FulfillmentOrder.Get returned %+v, expected %+v", fulfillment, expected) 63 | // } 64 | // } 65 | -------------------------------------------------------------------------------- /blog.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const blogsBasePath = "blogs" 10 | 11 | // BlogService is an interface for interfacing with the blogs endpoints 12 | // of the Shopify API. 13 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/blog 14 | type BlogService interface { 15 | List(context.Context, interface{}) ([]Blog, error) 16 | Count(context.Context, interface{}) (int, error) 17 | Get(context.Context, uint64, interface{}) (*Blog, error) 18 | Create(context.Context, Blog) (*Blog, error) 19 | Update(context.Context, Blog) (*Blog, error) 20 | Delete(context.Context, uint64) error 21 | } 22 | 23 | // BlogServiceOp handles communication with the blog related methods of 24 | // the Shopify API. 25 | type BlogServiceOp struct { 26 | client *Client 27 | } 28 | 29 | // Blog represents a Shopify blog 30 | type Blog struct { 31 | Id uint64 `json:"id"` 32 | Title string `json:"title"` 33 | Commentable string `json:"commentable"` 34 | Feedburner string `json:"feedburner"` 35 | FeedburnerLocation string `json:"feedburner_location"` 36 | Handle string `json:"handle"` 37 | Metafield Metafield `json:"metafield"` 38 | Tags string `json:"tags"` 39 | TemplateSuffix string `json:"template_suffix"` 40 | CreatedAt *time.Time `json:"created_at"` 41 | UpdatedAt *time.Time `json:"updated_at"` 42 | AdminGraphqlApiId string `json:"admin_graphql_api_id,omitempty"` 43 | } 44 | 45 | // BlogsResource is the result from the blogs.json endpoint 46 | type BlogsResource struct { 47 | Blogs []Blog `json:"blogs"` 48 | } 49 | 50 | // Represents the result from the blogs/X.json endpoint 51 | type BlogResource struct { 52 | Blog *Blog `json:"blog"` 53 | } 54 | 55 | // List all blogs 56 | func (s *BlogServiceOp) List(ctx context.Context, options interface{}) ([]Blog, error) { 57 | path := fmt.Sprintf("%s.json", blogsBasePath) 58 | resource := new(BlogsResource) 59 | err := s.client.Get(ctx, path, resource, options) 60 | return resource.Blogs, err 61 | } 62 | 63 | // Count blogs 64 | func (s *BlogServiceOp) Count(ctx context.Context, options interface{}) (int, error) { 65 | path := fmt.Sprintf("%s/count.json", blogsBasePath) 66 | return s.client.Count(ctx, path, options) 67 | } 68 | 69 | // Get single blog 70 | func (s *BlogServiceOp) Get(ctx context.Context, blogId uint64, options interface{}) (*Blog, error) { 71 | path := fmt.Sprintf("%s/%d.json", blogsBasePath, blogId) 72 | resource := new(BlogResource) 73 | err := s.client.Get(ctx, path, resource, options) 74 | return resource.Blog, err 75 | } 76 | 77 | // Create a new blog 78 | func (s *BlogServiceOp) Create(ctx context.Context, blog Blog) (*Blog, error) { 79 | path := fmt.Sprintf("%s.json", blogsBasePath) 80 | wrappedData := BlogResource{Blog: &blog} 81 | resource := new(BlogResource) 82 | err := s.client.Post(ctx, path, wrappedData, resource) 83 | return resource.Blog, err 84 | } 85 | 86 | // Update an existing blog 87 | func (s *BlogServiceOp) Update(ctx context.Context, blog Blog) (*Blog, error) { 88 | path := fmt.Sprintf("%s/%d.json", blogsBasePath, blog.Id) 89 | wrappedData := BlogResource{Blog: &blog} 90 | resource := new(BlogResource) 91 | err := s.client.Put(ctx, path, wrappedData, resource) 92 | return resource.Blog, err 93 | } 94 | 95 | // Delete an blog 96 | func (s *BlogServiceOp) Delete(ctx context.Context, blogId uint64) error { 97 | return s.client.Delete(ctx, fmt.Sprintf("%s/%d.json", blogsBasePath, blogId)) 98 | } 99 | -------------------------------------------------------------------------------- /blog_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/jarcoal/httpmock" 10 | ) 11 | 12 | func TestBlogList(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | httpmock.RegisterResponder( 17 | "GET", 18 | fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs.json", client.pathPrefix), 19 | httpmock.NewStringResponder( 20 | 200, 21 | `{"blogs": [{"id":1},{"id":2}]}`, 22 | ), 23 | ) 24 | 25 | blogs, err := client.Blog.List(context.Background(), nil) 26 | if err != nil { 27 | t.Errorf("Blog.List returned error: %v", err) 28 | } 29 | 30 | expected := []Blog{{Id: 1}, {Id: 2}} 31 | if !reflect.DeepEqual(blogs, expected) { 32 | t.Errorf("Blog.List returned %+v, expected %+v", blogs, expected) 33 | } 34 | } 35 | 36 | func TestBlogCount(t *testing.T) { 37 | setup() 38 | defer teardown() 39 | 40 | httpmock.RegisterResponder( 41 | "GET", 42 | fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/count.json", client.pathPrefix), 43 | httpmock.NewStringResponder( 44 | 200, 45 | `{"count": 5}`, 46 | ), 47 | ) 48 | 49 | cnt, err := client.Blog.Count(context.Background(), nil) 50 | if err != nil { 51 | t.Errorf("Blog.Count returned error: %v", err) 52 | } 53 | 54 | expected := 5 55 | if cnt != expected { 56 | t.Errorf("Blog.Count returned %d, expected %d", cnt, expected) 57 | } 58 | } 59 | 60 | func TestBlogGet(t *testing.T) { 61 | setup() 62 | defer teardown() 63 | 64 | httpmock.RegisterResponder( 65 | "GET", 66 | fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/1.json", client.pathPrefix), 67 | httpmock.NewStringResponder( 68 | 200, 69 | `{"blog": {"id":1}}`, 70 | ), 71 | ) 72 | 73 | blog, err := client.Blog.Get(context.Background(), 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 | func TestBlogCreate(t *testing.T) { 85 | setup() 86 | defer teardown() 87 | 88 | httpmock.RegisterResponder( 89 | "POST", 90 | fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs.json", client.pathPrefix), 91 | httpmock.NewBytesResponder( 92 | 200, 93 | loadFixture("blog.json"), 94 | ), 95 | ) 96 | 97 | blog := Blog{ 98 | Title: "Mah Blog", 99 | } 100 | 101 | returnedBlog, err := client.Blog.Create(context.Background(), blog) 102 | if err != nil { 103 | t.Errorf("Blog.Create returned error: %v", err) 104 | } 105 | 106 | expectedInt := uint64(241253187) 107 | if returnedBlog.Id != expectedInt { 108 | t.Errorf("Blog.Id returned %+v, expected %+v", returnedBlog.Id, expectedInt) 109 | } 110 | } 111 | 112 | func TestBlogUpdate(t *testing.T) { 113 | setup() 114 | defer teardown() 115 | 116 | httpmock.RegisterResponder( 117 | "PUT", 118 | fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/1.json", client.pathPrefix), 119 | httpmock.NewBytesResponder( 120 | 200, 121 | loadFixture("blog.json"), 122 | ), 123 | ) 124 | 125 | blog := Blog{ 126 | Id: 1, 127 | Title: "Mah Blog", 128 | } 129 | 130 | returnedBlog, err := client.Blog.Update(context.Background(), blog) 131 | if err != nil { 132 | t.Errorf("Blog.Update returned error: %v", err) 133 | } 134 | 135 | expectedInt := uint64(241253187) 136 | if returnedBlog.Id != expectedInt { 137 | t.Errorf("Blog.Id returned %+v, expected %+v", returnedBlog.Id, expectedInt) 138 | } 139 | } 140 | 141 | func TestBlogDelete(t *testing.T) { 142 | setup() 143 | defer teardown() 144 | 145 | httpmock.RegisterResponder("DELETE", fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/1.json", client.pathPrefix), 146 | httpmock.NewStringResponder(200, "{}")) 147 | 148 | err := client.Blog.Delete(context.Background(), 1) 149 | if err != nil { 150 | t.Errorf("Blog.Delete returned error: %v", err) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /collect.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const collectsBasePath = "collects" 10 | 11 | // CollectService is an interface for interfacing with the collect endpoints 12 | // of the Shopify API. 13 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/collect 14 | type CollectService interface { 15 | List(context.Context, interface{}) ([]Collect, error) 16 | Count(context.Context, interface{}) (int, error) 17 | Get(context.Context, uint64, interface{}) (*Collect, error) 18 | Create(context.Context, Collect) (*Collect, error) 19 | Delete(context.Context, uint64) error 20 | } 21 | 22 | // CollectServiceOp handles communication with the collect related methods of 23 | // the Shopify API. 24 | type CollectServiceOp struct { 25 | client *Client 26 | } 27 | 28 | // Collect represents a Shopify collect 29 | type Collect struct { 30 | Id uint64 `json:"id,omitempty"` 31 | CollectionId uint64 `json:"collection_id,omitempty"` 32 | ProductId uint64 `json:"product_id,omitempty"` 33 | Featured bool `json:"featured,omitempty"` 34 | CreatedAt *time.Time `json:"created_at,omitempty"` 35 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 36 | Position int `json:"position,omitempty"` 37 | SortValue string `json:"sort_value,omitempty"` 38 | } 39 | 40 | // Represents the result from the collects/X.json endpoint 41 | type CollectResource struct { 42 | Collect *Collect `json:"collect"` 43 | } 44 | 45 | // Represents the result from the collects.json endpoint 46 | type CollectsResource struct { 47 | Collects []Collect `json:"collects"` 48 | } 49 | 50 | // List collects 51 | func (s *CollectServiceOp) List(ctx context.Context, options interface{}) ([]Collect, error) { 52 | path := fmt.Sprintf("%s.json", collectsBasePath) 53 | resource := new(CollectsResource) 54 | err := s.client.Get(ctx, path, resource, options) 55 | return resource.Collects, err 56 | } 57 | 58 | // Count collects 59 | func (s *CollectServiceOp) Count(ctx context.Context, options interface{}) (int, error) { 60 | path := fmt.Sprintf("%s/count.json", collectsBasePath) 61 | return s.client.Count(ctx, path, options) 62 | } 63 | 64 | // Get individual collect 65 | func (s *CollectServiceOp) Get(ctx context.Context, collectId uint64, options interface{}) (*Collect, error) { 66 | path := fmt.Sprintf("%s/%d.json", collectsBasePath, collectId) 67 | resource := new(CollectResource) 68 | err := s.client.Get(ctx, path, resource, options) 69 | return resource.Collect, err 70 | } 71 | 72 | // Create collects 73 | func (s *CollectServiceOp) Create(ctx context.Context, collect Collect) (*Collect, error) { 74 | path := fmt.Sprintf("%s.json", collectsBasePath) 75 | wrappedData := CollectResource{Collect: &collect} 76 | resource := new(CollectResource) 77 | err := s.client.Post(ctx, path, wrappedData, resource) 78 | return resource.Collect, err 79 | } 80 | 81 | // Delete an existing collect 82 | func (s *CollectServiceOp) Delete(ctx context.Context, collectId uint64) error { 83 | return s.client.Delete(ctx, fmt.Sprintf("%s/%d.json", collectsBasePath, collectId)) 84 | } 85 | -------------------------------------------------------------------------------- /collect_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/jarcoal/httpmock" 10 | ) 11 | 12 | func collectTests(t *testing.T, collect Collect) { 13 | // Test a few fields 14 | cases := []struct { 15 | field string 16 | expected interface{} 17 | actual interface{} 18 | }{ 19 | {"Id", uint64(18091352323), collect.Id}, 20 | {"CollectionId", uint64(241600835), collect.CollectionId}, 21 | {"ProductId", uint64(6654094787), collect.ProductId}, 22 | {"Featured", false, collect.Featured}, 23 | {"SortValue", "0000000002", collect.SortValue}, 24 | } 25 | 26 | for _, c := range cases { 27 | if c.expected != c.actual { 28 | t.Errorf("Collect.%v returned %v, expected %v", c.field, c.actual, c.expected) 29 | } 30 | } 31 | } 32 | 33 | func TestCollectList(t *testing.T) { 34 | setup() 35 | defer teardown() 36 | 37 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/collects.json", client.pathPrefix), 38 | httpmock.NewStringResponder(200, `{"collects": [{"id":1},{"id":2}]}`)) 39 | 40 | collects, err := client.Collect.List(context.Background(), nil) 41 | if err != nil { 42 | t.Errorf("Collect.List returned error: %v", err) 43 | } 44 | 45 | expected := []Collect{{Id: 1}, {Id: 2}} 46 | if !reflect.DeepEqual(collects, expected) { 47 | t.Errorf("Collect.List returned %+v, expected %+v", collects, expected) 48 | } 49 | } 50 | 51 | func TestCollectCount(t *testing.T) { 52 | setup() 53 | defer teardown() 54 | 55 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/collects/count.json", client.pathPrefix), 56 | httpmock.NewStringResponder(200, `{"count": 5}`)) 57 | 58 | params := map[string]string{"since_id": "123"} 59 | httpmock.RegisterResponderWithQuery("GET", 60 | fmt.Sprintf("https://fooshop.myshopify.com/%s/collects/count.json", client.pathPrefix), 61 | params, 62 | httpmock.NewStringResponder(200, `{"count": 2}`)) 63 | 64 | cnt, err := client.Collect.Count(context.Background(), nil) 65 | if err != nil { 66 | t.Errorf("Collect.Count returned error: %v", err) 67 | } 68 | 69 | expected := 5 70 | if cnt != expected { 71 | t.Errorf("Collect.Count returned %d, expected %d", cnt, expected) 72 | } 73 | 74 | sinceId := uint64(123) 75 | cnt, err = client.Collect.Count(context.Background(), ListOptions{SinceId: &sinceId}) 76 | if err != nil { 77 | t.Errorf("Collect.Count returned error: %v", err) 78 | } 79 | 80 | expected = 2 81 | if cnt != expected { 82 | t.Errorf("Collect.Count returned %d, expected %d", cnt, expected) 83 | } 84 | } 85 | 86 | func TestCollectGet(t *testing.T) { 87 | setup() 88 | defer teardown() 89 | 90 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/collects/1.json", client.pathPrefix), 91 | httpmock.NewStringResponder(200, `{"collect": {"id":1}}`)) 92 | 93 | product, err := client.Collect.Get(context.Background(), 1, nil) 94 | if err != nil { 95 | t.Errorf("Collect.Get returned error: %v", err) 96 | } 97 | 98 | expected := &Collect{Id: 1} 99 | if !reflect.DeepEqual(product, expected) { 100 | t.Errorf("Collect.Get returned %+v, expected %+v", product, expected) 101 | } 102 | } 103 | 104 | func TestCollectCreate(t *testing.T) { 105 | setup() 106 | defer teardown() 107 | 108 | httpmock.RegisterResponder("POST", fmt.Sprintf("https://fooshop.myshopify.com/%s/collects.json", client.pathPrefix), 109 | httpmock.NewBytesResponder(200, loadFixture("collect.json"))) 110 | 111 | collect := Collect{ 112 | CollectionId: 241600835, 113 | ProductId: 6654094787, 114 | } 115 | 116 | returnedCollect, err := client.Collect.Create(context.Background(), collect) 117 | if err != nil { 118 | t.Errorf("Collect.Create returned error: %v", err) 119 | } 120 | 121 | collectTests(t, *returnedCollect) 122 | } 123 | 124 | func TestCollectDelete(t *testing.T) { 125 | setup() 126 | defer teardown() 127 | 128 | httpmock.RegisterResponder("DELETE", fmt.Sprintf("https://fooshop.myshopify.com/%s/collects/1.json", client.pathPrefix), 129 | httpmock.NewStringResponder(200, "{}")) 130 | 131 | err := client.Collect.Delete(context.Background(), 1) 132 | if err != nil { 133 | t.Errorf("Collect.Delete returned error: %v", err) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /collection.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const collectionsBasePath = "collections" 10 | 11 | // CollectionService is an interface for interfacing with the collection endpoints 12 | // of the Shopify API. 13 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/collection 14 | type CollectionService interface { 15 | Get(ctx context.Context, collectionId uint64, options interface{}) (*Collection, error) 16 | ListProducts(ctx context.Context, collectionId uint64, options interface{}) ([]Product, error) 17 | ListProductsWithPagination(ctx context.Context, collectionId uint64, options interface{}) ([]Product, *Pagination, error) 18 | } 19 | 20 | // CollectionServiceOp handles communication with the collection related methods of 21 | // the Shopify API. 22 | type CollectionServiceOp struct { 23 | client *Client 24 | } 25 | 26 | // Collection represents a Shopify collection 27 | type Collection struct { 28 | Id uint64 `json:"id"` 29 | Handle string `json:"handle"` 30 | Title string `json:"title"` 31 | UpdatedAt *time.Time `json:"updated_at"` 32 | BodyHTML string `json:"body_html"` 33 | SortOrder string `json:"sort_order"` 34 | TemplateSuffix string `json:"template_suffix"` 35 | Image Image `json:"image"` 36 | PublishedAt *time.Time `json:"published_at"` 37 | PublishedScope string `json:"published_scope"` 38 | } 39 | 40 | // Represents the result from the collections/X.json endpoint 41 | type CollectionResource struct { 42 | Collection *Collection `json:"collection"` 43 | } 44 | 45 | // Get individual collection 46 | func (s *CollectionServiceOp) Get(ctx context.Context, collectionId uint64, options interface{}) (*Collection, error) { 47 | path := fmt.Sprintf("%s/%d.json", collectionsBasePath, collectionId) 48 | resource := new(CollectionResource) 49 | err := s.client.Get(ctx, path, resource, options) 50 | return resource.Collection, err 51 | } 52 | 53 | // List products for a collection 54 | func (s *CollectionServiceOp) ListProducts(ctx context.Context, collectionId uint64, options interface{}) ([]Product, error) { 55 | products, _, err := s.ListProductsWithPagination(ctx, collectionId, options) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return products, nil 60 | } 61 | 62 | // List products for a collection and return pagination to retrieve next/previous results. 63 | func (s *CollectionServiceOp) ListProductsWithPagination(ctx context.Context, collectionId uint64, options interface{}) ([]Product, *Pagination, error) { 64 | path := fmt.Sprintf("%s/%d/products.json", collectionsBasePath, collectionId) 65 | resource := new(ProductsResource) 66 | 67 | pagination, err := s.client.ListWithPagination(ctx, path, resource, options) 68 | if err != nil { 69 | return nil, nil, err 70 | } 71 | 72 | return resource.Products, pagination, nil 73 | } 74 | -------------------------------------------------------------------------------- /customer_address.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // CustomerAddressService is an interface for interfacing with the customer address endpoints 9 | // of the Shopify API. 10 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/customer_address 11 | type CustomerAddressService interface { 12 | List(context.Context, uint64, interface{}) ([]CustomerAddress, error) 13 | Get(context.Context, uint64, uint64, interface{}) (*CustomerAddress, error) 14 | Create(context.Context, uint64, CustomerAddress) (*CustomerAddress, error) 15 | Update(context.Context, uint64, CustomerAddress) (*CustomerAddress, error) 16 | Delete(context.Context, uint64, uint64) error 17 | } 18 | 19 | // CustomerAddressServiceOp handles communication with the customer address related methods of 20 | // the Shopify API. 21 | type CustomerAddressServiceOp struct { 22 | client *Client 23 | } 24 | 25 | // CustomerAddress represents a Shopify customer address 26 | type CustomerAddress struct { 27 | Id uint64 `json:"id,omitempty"` 28 | CustomerId uint64 `json:"customer_id,omitempty"` 29 | FirstName string `json:"first_name,omitempty"` 30 | LastName string `json:"last_name,omitempty"` 31 | Company string `json:"company,omitempty"` 32 | Address1 string `json:"address1,omitempty"` 33 | Address2 string `json:"address2,omitempty"` 34 | City string `json:"city,omitempty"` 35 | Province string `json:"province,omitempty"` 36 | Country string `json:"country,omitempty"` 37 | Zip string `json:"zip,omitempty"` 38 | Phone string `json:"phone,omitempty"` 39 | Name string `json:"name,omitempty"` 40 | ProvinceCode string `json:"province_code,omitempty"` 41 | CountryCode string `json:"country_code,omitempty"` 42 | CountryName string `json:"country_name,omitempty"` 43 | Default bool `json:"default,omitempty"` 44 | } 45 | 46 | // CustomerAddressResoruce represents the result from the addresses/X.json endpoint 47 | type CustomerAddressResource struct { 48 | Address *CustomerAddress `json:"customer_address"` 49 | } 50 | 51 | // CustomerAddressResoruce represents the result from the customers/X/addresses.json endpoint 52 | type CustomerAddressesResource struct { 53 | Addresses []CustomerAddress `json:"addresses"` 54 | } 55 | 56 | // List addresses 57 | func (s *CustomerAddressServiceOp) List(ctx context.Context, customerId uint64, options interface{}) ([]CustomerAddress, error) { 58 | path := fmt.Sprintf("%s/%d/addresses.json", customersBasePath, customerId) 59 | resource := new(CustomerAddressesResource) 60 | err := s.client.Get(ctx, path, resource, options) 61 | return resource.Addresses, err 62 | } 63 | 64 | // Get address 65 | func (s *CustomerAddressServiceOp) Get(ctx context.Context, customerId, addressId uint64, options interface{}) (*CustomerAddress, error) { 66 | path := fmt.Sprintf("%s/%d/addresses/%d.json", customersBasePath, customerId, addressId) 67 | resource := new(CustomerAddressResource) 68 | err := s.client.Get(ctx, path, resource, options) 69 | return resource.Address, err 70 | } 71 | 72 | // Create a new address for given customer 73 | func (s *CustomerAddressServiceOp) Create(ctx context.Context, customerId uint64, address CustomerAddress) (*CustomerAddress, error) { 74 | path := fmt.Sprintf("%s/%d/addresses.json", customersBasePath, customerId) 75 | wrappedData := CustomerAddressResource{Address: &address} 76 | resource := new(CustomerAddressResource) 77 | err := s.client.Post(ctx, path, wrappedData, resource) 78 | return resource.Address, err 79 | } 80 | 81 | // Create a new address for given customer 82 | func (s *CustomerAddressServiceOp) Update(ctx context.Context, customerId uint64, address CustomerAddress) (*CustomerAddress, error) { 83 | path := fmt.Sprintf("%s/%d/addresses/%d.json", customersBasePath, customerId, address.Id) 84 | wrappedData := CustomerAddressResource{Address: &address} 85 | resource := new(CustomerAddressResource) 86 | err := s.client.Put(ctx, path, wrappedData, resource) 87 | return resource.Address, err 88 | } 89 | 90 | // Delete an existing address 91 | func (s *CustomerAddressServiceOp) Delete(ctx context.Context, customerId, addressId uint64) error { 92 | return s.client.Delete(ctx, fmt.Sprintf("%s/%d/addresses/%d.json", customersBasePath, customerId, addressId)) 93 | } 94 | -------------------------------------------------------------------------------- /discount_code.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const discountCodeBasePath = "price_rules/%d/discount_codes" 10 | 11 | // DiscountCodeService is an interface for interfacing with the discount endpoints 12 | // of the Shopify API. 13 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/PriceRuleDiscountCode 14 | type DiscountCodeService interface { 15 | Create(context.Context, uint64, PriceRuleDiscountCode) (*PriceRuleDiscountCode, error) 16 | Update(context.Context, uint64, PriceRuleDiscountCode) (*PriceRuleDiscountCode, error) 17 | List(context.Context, uint64) ([]PriceRuleDiscountCode, error) 18 | Get(context.Context, uint64, uint64) (*PriceRuleDiscountCode, error) 19 | Delete(context.Context, uint64, uint64) error 20 | } 21 | 22 | // DiscountCodeServiceOp handles communication with the discount code 23 | // related methods of the Shopify API. 24 | type DiscountCodeServiceOp struct { 25 | client *Client 26 | } 27 | 28 | // PriceRuleDiscountCode represents a Shopify Discount Code 29 | type PriceRuleDiscountCode struct { 30 | Id uint64 `json:"id,omitempty"` 31 | PriceRuleId uint64 `json:"price_rule_id,omitempty"` 32 | Code string `json:"code,omitempty"` 33 | UsageCount int `json:"usage_count,omitempty"` 34 | CreatedAt *time.Time `json:"created_at,omitempty"` 35 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 36 | } 37 | 38 | // DiscountCodesResource is the result from the discount_codes.json endpoint 39 | type DiscountCodesResource struct { 40 | DiscountCodes []PriceRuleDiscountCode `json:"discount_codes"` 41 | } 42 | 43 | // DiscountCodeResource represents the result from the discount_codes/X.json endpoint 44 | type DiscountCodeResource struct { 45 | PriceRuleDiscountCode *PriceRuleDiscountCode `json:"discount_code"` 46 | } 47 | 48 | // Create a discount code 49 | func (s *DiscountCodeServiceOp) Create(ctx context.Context, priceRuleId uint64, dc PriceRuleDiscountCode) (*PriceRuleDiscountCode, error) { 50 | path := fmt.Sprintf(discountCodeBasePath+".json", priceRuleId) 51 | wrappedData := DiscountCodeResource{PriceRuleDiscountCode: &dc} 52 | resource := new(DiscountCodeResource) 53 | err := s.client.Post(ctx, path, wrappedData, resource) 54 | return resource.PriceRuleDiscountCode, err 55 | } 56 | 57 | // Update an existing discount code 58 | func (s *DiscountCodeServiceOp) Update(ctx context.Context, priceRuleId uint64, dc PriceRuleDiscountCode) (*PriceRuleDiscountCode, error) { 59 | path := fmt.Sprintf(discountCodeBasePath+"/%d.json", priceRuleId, dc.Id) 60 | wrappedData := DiscountCodeResource{PriceRuleDiscountCode: &dc} 61 | resource := new(DiscountCodeResource) 62 | err := s.client.Put(ctx, path, wrappedData, resource) 63 | return resource.PriceRuleDiscountCode, err 64 | } 65 | 66 | // List of discount codes 67 | func (s *DiscountCodeServiceOp) List(ctx context.Context, priceRuleId uint64) ([]PriceRuleDiscountCode, error) { 68 | path := fmt.Sprintf(discountCodeBasePath+".json", priceRuleId) 69 | resource := new(DiscountCodesResource) 70 | err := s.client.Get(ctx, path, resource, nil) 71 | return resource.DiscountCodes, err 72 | } 73 | 74 | // Get a single discount code 75 | func (s *DiscountCodeServiceOp) Get(ctx context.Context, priceRuleId uint64, discountCodeId uint64) (*PriceRuleDiscountCode, error) { 76 | path := fmt.Sprintf(discountCodeBasePath+"/%d.json", priceRuleId, discountCodeId) 77 | resource := new(DiscountCodeResource) 78 | err := s.client.Get(ctx, path, resource, nil) 79 | return resource.PriceRuleDiscountCode, err 80 | } 81 | 82 | // Delete a discount code 83 | func (s *DiscountCodeServiceOp) Delete(ctx context.Context, priceRuleId uint64, discountCodeId uint64) error { 84 | return s.client.Delete(ctx, fmt.Sprintf(discountCodeBasePath+"/%d.json", priceRuleId, discountCodeId)) 85 | } 86 | -------------------------------------------------------------------------------- /discount_code_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/jarcoal/httpmock" 9 | ) 10 | 11 | func TestDiscountCodeList(t *testing.T) { 12 | setup() 13 | defer teardown() 14 | 15 | httpmock.RegisterResponder( 16 | "GET", 17 | fmt.Sprintf("https://fooshop.myshopify.com/%s/price_rules/507328175/discount_codes.json", client.pathPrefix), 18 | httpmock.NewStringResponder( 19 | 200, 20 | `{"discount_codes":[{"id":507328175,"price_rule_id":507328175,"code":"SUMMERSALE10OFF","usage_count":0,"created_at":"2018-07-05T12:41:00-04:00","updated_at":"2018-07-05T12:41:00-04:00"}]}`, 21 | ), 22 | ) 23 | 24 | codes, err := client.DiscountCode.List(context.Background(), 507328175) 25 | if err != nil { 26 | t.Errorf("DiscountCode.List returned error: %v", err) 27 | } 28 | 29 | expected := []PriceRuleDiscountCode{{Id: 507328175}} 30 | if expected[0].Id != codes[0].Id { 31 | t.Errorf("DiscountCode.List returned %+v, expected %+v", codes, expected) 32 | } 33 | } 34 | 35 | func TestDiscountCodeGet(t *testing.T) { 36 | setup() 37 | defer teardown() 38 | 39 | httpmock.RegisterResponder( 40 | "GET", 41 | fmt.Sprintf("https://fooshop.myshopify.com/%s/price_rules/507328175/discount_codes/507328175.json", client.pathPrefix), 42 | httpmock.NewStringResponder( 43 | 200, 44 | `{"discount_code":{"id":507328175,"price_rule_id":507328175,"code":"SUMMERSALE10OFF","usage_count":0,"created_at":"2018-07-05T12:41:00-04:00","updated_at":"2018-07-05T12:41:00-04:00"}}`, 45 | ), 46 | ) 47 | 48 | dc, err := client.DiscountCode.Get(context.Background(), 507328175, 507328175) 49 | if err != nil { 50 | t.Errorf("DiscountCode.Get returned error: %v", err) 51 | } 52 | 53 | expected := &PriceRuleDiscountCode{Id: 507328175} 54 | 55 | if dc.Id != expected.Id { 56 | t.Errorf("DiscountCode.Get returned %+v, expected %+v", dc, expected) 57 | } 58 | } 59 | 60 | func TestDiscountCodeCreate(t *testing.T) { 61 | setup() 62 | defer teardown() 63 | 64 | httpmock.RegisterResponder( 65 | "POST", 66 | fmt.Sprintf("https://fooshop.myshopify.com/%s/price_rules/507328175/discount_codes.json", client.pathPrefix), 67 | httpmock.NewBytesResponder( 68 | 201, 69 | loadFixture("discount_code.json"), 70 | ), 71 | ) 72 | 73 | dc := PriceRuleDiscountCode{ 74 | Code: "SUMMERSALE10OFF", 75 | } 76 | 77 | returnedDC, err := client.DiscountCode.Create(context.Background(), 507328175, dc) 78 | if err != nil { 79 | t.Errorf("DiscountCode.Create returned error: %v", err) 80 | } 81 | 82 | expectedInt := uint64(1054381139) 83 | if returnedDC.Id != expectedInt { 84 | t.Errorf("DiscountCode.Id returned %+v, expected %+v", returnedDC.Id, expectedInt) 85 | } 86 | } 87 | 88 | func TestDiscountCodeUpdate(t *testing.T) { 89 | setup() 90 | defer teardown() 91 | 92 | httpmock.RegisterResponder( 93 | "PUT", 94 | fmt.Sprintf("https://fooshop.myshopify.com/%s/price_rules/507328175/discount_codes/1054381139.json", client.pathPrefix), 95 | httpmock.NewBytesResponder( 96 | 200, 97 | loadFixture("discount_code.json"), 98 | ), 99 | ) 100 | 101 | dc := PriceRuleDiscountCode{ 102 | Id: uint64(1054381139), 103 | Code: "SUMMERSALE10OFF", 104 | } 105 | 106 | returnedDC, err := client.DiscountCode.Update(context.Background(), 507328175, dc) 107 | if err != nil { 108 | t.Errorf("DiscountCode.Update returned error: %v", err) 109 | } 110 | 111 | expectedInt := uint64(1054381139) 112 | if returnedDC.Id != expectedInt { 113 | t.Errorf("DiscountCode.Id returned %+v, expected %+v", returnedDC.Id, expectedInt) 114 | } 115 | } 116 | 117 | func TestDiscountCodeDelete(t *testing.T) { 118 | setup() 119 | defer teardown() 120 | 121 | httpmock.RegisterResponder("DELETE", fmt.Sprintf("https://fooshop.myshopify.com/%s/price_rules/507328175/discount_codes/507328175.json", client.pathPrefix), 122 | httpmock.NewStringResponder(204, "{}")) 123 | 124 | err := client.DiscountCode.Delete(context.Background(), 507328175, 507328175) 125 | if err != nil { 126 | t.Errorf("DiscountCode.Delete returned error: %v", err) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | test: 5 | image: go-shopify:latest 6 | build: . 7 | command: go test -v -cover ./... 8 | volumes: 9 | - .:/go/src/github.com/bold-commerce/go-shopify 10 | -------------------------------------------------------------------------------- /fixtures/access_scopes.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_scopes": [ 3 | { 4 | "handle": "scope_1" 5 | }, 6 | { 7 | "handle": "scope_2" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /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/api/9999-99/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/carrier_service.json: -------------------------------------------------------------------------------- 1 | { 2 | "carrier_service": { 3 | "id": 1, 4 | "name": "Shipping Rate Provider", 5 | "active": true, 6 | "service_discovery": true, 7 | "carrier_service_type": "api", 8 | "admin_graphql_api_id": "gid://shopify/DeliveryCarrierService/1", 9 | "format": "json", 10 | "callback_url": "https://fooshop.example.com/shipping" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /fixtures/carrier_services.json: -------------------------------------------------------------------------------- 1 | { 2 | "carrier_services": [ 3 | { 4 | "id": 1, 5 | "name": "Shipping Rate Provider", 6 | "active": true, 7 | "service_discovery": true, 8 | "carrier_service_type": "api", 9 | "admin_graphql_api_id": "gid://shopify/DeliveryCarrierService/1", 10 | "format": "json", 11 | "callback_url": "https://fooshop.example.com/shipping" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /fixtures/collect.json: -------------------------------------------------------------------------------- 1 | { 2 | "collect": { 3 | "id": 18091352323, 4 | "collection_id": 241600835, 5 | "product_id": 6654094787, 6 | "created_at": "2021-02-05T20:39:25-05:00", 7 | "updated_at": "2021-02-05T20:39:25-05:00", 8 | "position": 2, 9 | "sort_value": "0000000002" 10 | } 11 | } -------------------------------------------------------------------------------- /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-00:00", 7 | "updated_at": "2017-09-23T18:15:47-00: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 | "accepts_marketing_updated_at": "2017-09-23T18:15:47-00:00", 43 | "email_marketing_consent": { 44 | "state": "not_subscribed", 45 | "opt_in_level": "single_opt_in", 46 | "consent_updated_at": "2017-09-23T18:15:47-00:00" 47 | }, 48 | "sms_marketing_consent": { 49 | "state": "not_subscribed", 50 | "opt_in_level": "single_opt_in", 51 | "consent_updated_at": "2017-09-23T18:15:47-00:00", 52 | "consent_collected_from": "OTHER" 53 | }, 54 | "default_address": { 55 | "id": 1, 56 | "customer_id": 1, 57 | "first_name": "Test", 58 | "last_name": "Citizen", 59 | "company": null, 60 | "address1": "1 Smith St", 61 | "address2": null, 62 | "city": "BRISBANE", 63 | "province": "Queensland", 64 | "country": "Australia", 65 | "zip": "4000", 66 | "phone": "1111 111 111", 67 | "name": "Test Citizen", 68 | "province_code": "QLD", 69 | "country_code": "AU", 70 | "country_name": "Australia", 71 | "default": true 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /fixtures/customer_address.json: -------------------------------------------------------------------------------- 1 | { 2 | "customer_address": { 3 | "address1": "1 Smith St", 4 | "address2": null, 5 | "city": "BRISBANE", 6 | "company": "TestCo", 7 | "country": "Australia", 8 | "country_code": "AU", 9 | "country_name": "Australia", 10 | "customer_id": 1, 11 | "default": true, 12 | "first_name": "Test", 13 | "id": 1, 14 | "last_name": "Citizen", 15 | "name": "Test Citizen", 16 | "phone": "1111 111 111", 17 | "province": "Queensland", 18 | "province_code": "QLD", 19 | "zip": "4000" 20 | } 21 | } -------------------------------------------------------------------------------- /fixtures/customer_addresses.json: -------------------------------------------------------------------------------- 1 | { 2 | "addresses": [ 3 | { 4 | "address1": "1 Smith St", 5 | "address2": null, 6 | "city": "BRISBANE", 7 | "company": "TestCo", 8 | "country": "Australia", 9 | "country_code": "AU", 10 | "country_name": "Australia", 11 | "customer_id": 1, 12 | "default": true, 13 | "first_name": "Test", 14 | "id": 1, 15 | "last_name": "Citizen", 16 | "name": "Test Citizen", 17 | "phone": "1111 111 111", 18 | "province": "Queensland", 19 | "province_code": "QLD", 20 | "zip": "4000" 21 | }, 22 | { 23 | "address1": "2 Smith St", 24 | "address2": null, 25 | "city": "BRISBANE", 26 | "company": null, 27 | "country": "Australia", 28 | "country_code": "AU", 29 | "country_name": "Australia", 30 | "customer_id": 1, 31 | "default": true, 32 | "first_name": "Test", 33 | "id": 2, 34 | "last_name": "Citizen", 35 | "name": "Test Citizen", 36 | "phone": "2222 222 222", 37 | "province": "Queensland", 38 | "province_code": "QLD", 39 | "zip": "4000" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /fixtures/customer_tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": [ 3 | "tag1", 4 | "another-tag" 5 | ] 6 | } -------------------------------------------------------------------------------- /fixtures/discount_code.json: -------------------------------------------------------------------------------- 1 | { 2 | "discount_code": { 3 | "id": 1054381139, 4 | "price_rule_id": 507328175, 5 | "code": "SUMMERSALE10OFF", 6 | "usage_count": 0, 7 | "created_at": "2018-07-05T13:04:26-04:00", 8 | "updated_at": "2018-07-05T13:04:26-04:00" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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/fulfillment_accept.json: -------------------------------------------------------------------------------- 1 | { 2 | "fulfillment_order": { 3 | "id": 1046000828, 4 | "shop_id": 548380009, 5 | "order_id": 450789469, 6 | "assigned_location_id": 24826418, 7 | "request_status": "accepted", 8 | "status": "in_progress", 9 | "supported_actions": ["request_cancellation", "create_fulfillment"], 10 | "destination": { 11 | "id": 1046000815, 12 | "address1": "Chestnut Street 92", 13 | "address2": "", 14 | "city": "Louisville", 15 | "company": null, 16 | "country": "United States", 17 | "email": "bob.norman@mail.example.com", 18 | "first_name": "Bob", 19 | "last_name": "Norman", 20 | "phone": "+1(502)-459-2181", 21 | "province": "Kentucky", 22 | "zip": "40202" 23 | }, 24 | "origin": { 25 | "address1": null, 26 | "address2": null, 27 | "city": null, 28 | "country_code": "DE", 29 | "location_id": 24826418, 30 | "name": "Apple Api Shipwire", 31 | "phone": null, 32 | "province": null, 33 | "zip": null 34 | }, 35 | "line_items": [ 36 | { 37 | "id": 1058737564, 38 | "shop_id": 548380009, 39 | "fulfillment_order_id": 1046000828, 40 | "quantity": 1, 41 | "line_item_id": 466157049, 42 | "inventory_item_id": 39072856, 43 | "fulfillable_quantity": 1, 44 | "variant_id": 39072856 45 | }, 46 | { 47 | "id": 1058737565, 48 | "shop_id": 548380009, 49 | "fulfillment_order_id": 1046000828, 50 | "quantity": 1, 51 | "line_item_id": 518995019, 52 | "inventory_item_id": 49148385, 53 | "fulfillable_quantity": 1, 54 | "variant_id": 49148385 55 | }, 56 | { 57 | "id": 1058737566, 58 | "shop_id": 548380009, 59 | "fulfillment_order_id": 1046000828, 60 | "quantity": 1, 61 | "line_item_id": 703073504, 62 | "inventory_item_id": 457924702, 63 | "fulfillable_quantity": 1, 64 | "variant_id": 457924702 65 | } 66 | ], 67 | "outgoing_requests": [], 68 | "fulfillment_service_handle": "shipwire-app" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /fixtures/fulfillment_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "fulfillment_event": { 3 | "id": 944956393, 4 | "fulfillment_id": 255858046, 5 | "status": "in_transit", 6 | "message": null, 7 | "happened_at": "2023-10-20T23:39:27-04:00", 8 | "city": null, 9 | "province": null, 10 | "country": null, 11 | "zip": null, 12 | "address1": null, 13 | "latitude": null, 14 | "longitude": null, 15 | "shop_id": 548380009, 16 | "created_at": "2023-10-20T23:39:27-04:00", 17 | "updated_at": "2023-10-20T23:39:27-04:00", 18 | "estimated_delivery_at": null, 19 | "order_id": 450789469, 20 | "admin_graphql_api_id": "gid://shopify/FulfillmentEvent/944956393" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/fulfillment_events.json: -------------------------------------------------------------------------------- 1 | { 2 | "fulfillment_events": [ 3 | { 4 | "id": 944956391, 5 | "fulfillment_id": 255858046, 6 | "status": "in_transit", 7 | "message": null, 8 | "happened_at": "2023-10-20T23:39:23-04:00", 9 | "city": null, 10 | "province": null, 11 | "country": null, 12 | "zip": null, 13 | "address1": null, 14 | "latitude": null, 15 | "longitude": null, 16 | "shop_id": 548380009, 17 | "created_at": "2023-10-20T23:39:23-04:00", 18 | "updated_at": "2023-10-20T23:39:23-04:00", 19 | "estimated_delivery_at": null, 20 | "order_id": 450789469, 21 | "admin_graphql_api_id": "gid://shopify/FulfillmentEvent/944956391" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/fulfillment_order.json: -------------------------------------------------------------------------------- 1 | { 2 | "fulfillment_order": { 3 | "assigned_location_id": 3183479, 4 | "destination": { 5 | "id": 54433189, 6 | "address1": "123 Amoebobacterieae St", 7 | "address2": "Unit 806", 8 | "city": "Ottawa", 9 | "company": "", 10 | "country": "Canada", 11 | "email": "bob@example.com", 12 | "first_name": "Bob", 13 | "last_name": "Bobsen", 14 | "phone": "(555)555-5555", 15 | "province": "Ontario", 16 | "zip": "K2P0V6" 17 | }, 18 | "delivery_method": { 19 | "id": 64434189, 20 | "method_type": "local", 21 | "min_delivery_date_time": "2022-04-20T23:59:59-04:00", 22 | "max_delivery_date_time": "2022-04-28T23:59:59-04:00" 23 | }, 24 | "fulfill_at": "2021-01-01T07:00:00-04:00", 25 | "fulfill_by": "2021-01-01T07:00:00-04:00", 26 | "fulfillment_holds": [ 27 | { 28 | "reason": "incorrect_address", 29 | "reason_notes": "the apartment number is missing." 30 | } 31 | ], 32 | "id": 255858046, 33 | "international_duties": { 34 | "incoterm": "DAP" 35 | }, 36 | "line_items": [ 37 | { 38 | "id": 466157049, 39 | "shop_id": 3998762, 40 | "fulfillment_order_id": 1568020, 41 | "line_item_id": 466157049, 42 | "inventory_item_id": 6588097, 43 | "quantity": 1, 44 | "fulfillable_quantity": 1, 45 | "variant_id": 2385087 46 | } 47 | ], 48 | "order_id": 3183479, 49 | "request_status": "unsubmitted", 50 | "shop_id": 255858046, 51 | "status": "open", 52 | "supported_actions": [ 53 | "create_fulfillment", 54 | "request_fulfillment", 55 | "cancel_fulfillment_order", 56 | "request_cancellation" 57 | ], 58 | "merchant_requests": [ 59 | { 60 | "message": "Hello, World!", 61 | "request_options": { 62 | "shipping_method": "pidgeon carrier", 63 | "note": "handle with care", 64 | "date": "2019-08-13T16:09:58-04:00" 65 | }, 66 | "kind": "fulfillment_request" 67 | } 68 | ], 69 | "assigned_location": { 70 | "address1": "123 Amoebobacterieae St", 71 | "address2": "Unit 806", 72 | "city": "Ottawa", 73 | "country_code": "CA", 74 | "location_id": 17232953366, 75 | "name": "Bob Bobsen", 76 | "phone": "(555)555-5555", 77 | "province": "Ontario", 78 | "zip": "K2P0V6" 79 | }, 80 | "created_at": "2022-01-01T11:00:00-01:00", 81 | "updated_at": "2022-01-01T11:00:00-01:00" 82 | } 83 | } -------------------------------------------------------------------------------- /fixtures/fulfillment_order_move.json: -------------------------------------------------------------------------------- 1 | { 2 | "original_fulfillment_order": { 3 | "id": 1046000818, 4 | "shop_id": 548380009, 5 | "order_id": 450789469, 6 | "assigned_location_id": 487838322, 7 | "request_status": "submitted", 8 | "status": "closed", 9 | "supported_actions": [], 10 | "destination": { 11 | "id": 1046000813, 12 | "address1": "Chestnut Street 92", 13 | "address2": "", 14 | "city": "Louisville", 15 | "company": null, 16 | "country": "United States", 17 | "email": "bob.norman@mail.example.com", 18 | "first_name": "Bob", 19 | "last_name": "Norman", 20 | "phone": "+1(502)-459-2181", 21 | "province": "Kentucky", 22 | "zip": "40202" 23 | }, 24 | "line_items": [ 25 | { 26 | "id": 1058737557, 27 | "shop_id": 548380009, 28 | "fulfillment_order_id": 1046000818, 29 | "quantity": 1, 30 | "line_item_id": 518995019, 31 | "inventory_item_id": 49148385, 32 | "fulfillable_quantity": 1, 33 | "variant_id": 49148385 34 | } 35 | ], 36 | "fulfillment_service_handle": "manual", 37 | "assigned_location": { 38 | "address1": null, 39 | "address2": null, 40 | "city": null, 41 | "country_code": "DE", 42 | "location_id": 24826418, 43 | "name": "Apple Api Shipwire", 44 | "phone": null, 45 | "province": null, 46 | "zip": null 47 | }, 48 | "merchant_requests": [] 49 | }, 50 | "moved_fulfillment_order": { 51 | "id": 1046000819, 52 | "shop_id": 548380009, 53 | "order_id": 450789469, 54 | "assigned_location_id": 655441491, 55 | "request_status": "unsubmitted", 56 | "status": "open", 57 | "supported_actions": [ 58 | "create_fulfillment", 59 | "move" 60 | ], 61 | "destination": { 62 | "id": 1046000814, 63 | "address1": "Chestnut Street 92", 64 | "address2": "", 65 | "city": "Louisville", 66 | "company": null, 67 | "country": "United States", 68 | "email": "bob.norman@mail.example.com", 69 | "first_name": "Bob", 70 | "last_name": "Norman", 71 | "phone": "+1(502)-459-2181", 72 | "province": "Kentucky", 73 | "zip": "40202" 74 | }, 75 | "line_items": [ 76 | { 77 | "id": 1058737558, 78 | "shop_id": 548380009, 79 | "fulfillment_order_id": 1046000819, 80 | "quantity": 1, 81 | "line_item_id": 518995019, 82 | "inventory_item_id": 49148385, 83 | "fulfillable_quantity": 1, 84 | "variant_id": 49148385 85 | } 86 | ], 87 | "fulfillment_service_handle": "manual", 88 | "assigned_location": { 89 | "address1": "50 Rideau Street", 90 | "address2": null, 91 | "city": "Ottawa", 92 | "country_code": "CA", 93 | "location_id": 655441491, 94 | "name": "50 Rideau Street", 95 | "phone": null, 96 | "province": "Ontario", 97 | "zip": "K1N 9J7" 98 | }, 99 | "merchant_requests": [] 100 | }, 101 | "remaining_fulfillment_order": null 102 | } -------------------------------------------------------------------------------- /fixtures/fulfillment_reject.json: -------------------------------------------------------------------------------- 1 | { 2 | "fulfillment_order": { 3 | "id": 1046000830, 4 | "shop_id": 548380009, 5 | "order_id": 450789469, 6 | "assigned_location_id": 24826418, 7 | "request_status": "rejected", 8 | "status": "open", 9 | "supported_actions": ["request_fulfillment", "create_fulfillment"], 10 | "destination": { 11 | "id": 1046000817, 12 | "address1": "Chestnut Street 92", 13 | "address2": "", 14 | "city": "Louisville", 15 | "company": null, 16 | "country": "United States", 17 | "email": "bob.norman@mail.example.com", 18 | "first_name": "Bob", 19 | "last_name": "Norman", 20 | "phone": "+1(502)-459-2181", 21 | "province": "Kentucky", 22 | "zip": "40202" 23 | }, 24 | "origin": { 25 | "address1": null, 26 | "address2": null, 27 | "city": null, 28 | "country_code": "DE", 29 | "location_id": 24826418, 30 | "name": "Apple Api Shipwire", 31 | "phone": null, 32 | "province": null, 33 | "zip": null 34 | }, 35 | "line_items": [ 36 | { 37 | "id": 1058737570, 38 | "shop_id": 548380009, 39 | "fulfillment_order_id": 1046000830, 40 | "quantity": 1, 41 | "line_item_id": 466157049, 42 | "inventory_item_id": 39072856, 43 | "fulfillable_quantity": 1, 44 | "variant_id": 39072856 45 | }, 46 | { 47 | "id": 1058737571, 48 | "shop_id": 548380009, 49 | "fulfillment_order_id": 1046000830, 50 | "quantity": 1, 51 | "line_item_id": 518995019, 52 | "inventory_item_id": 49148385, 53 | "fulfillable_quantity": 1, 54 | "variant_id": 49148385 55 | }, 56 | { 57 | "id": 1058737572, 58 | "shop_id": 548380009, 59 | "fulfillment_order_id": 1046000830, 60 | "quantity": 1, 61 | "line_item_id": 703073504, 62 | "inventory_item_id": 457924702, 63 | "fulfillable_quantity": 1, 64 | "variant_id": 457924702 65 | } 66 | ], 67 | "outgoing_requests": [], 68 | "fulfillment_service_handle": "shipwire-app" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /fixtures/fulfillment_service.json: -------------------------------------------------------------------------------- 1 | { 2 | "fulfillment_service": { 3 | "id": 1061774487, 4 | "name": "Jupiter Fulfillment", 5 | "email": "aaa@gmail.com", 6 | "service_name": "Jupiter Fulfillment", 7 | "handle": "jupiter-fulfillment", 8 | "fulfillment_orders_opt_in": false, 9 | "include_pending_stock": false, 10 | "provider_id": 1234, 11 | "location_id": 1072404542, 12 | "callback_url": "https://google.com/", 13 | "tracking_support": false, 14 | "inventory_management": false, 15 | "admin_graphql_api_id": "gid://shopify/ApiFulfillmentService/1061774487", 16 | "permits_sku_sharing": false 17 | } 18 | } -------------------------------------------------------------------------------- /fixtures/fulfillment_services.json: -------------------------------------------------------------------------------- 1 | { 2 | "fulfillment_services": [ 3 | { 4 | "id": 1061774487, 5 | "name": "Jupiter Fulfillment", 6 | "email": "aaa@gmail.com", 7 | "service_name": "Jupiter Fulfillment", 8 | "handle": "jupiter-fulfillment", 9 | "fulfillment_orders_opt_in": false, 10 | "include_pending_stock": false, 11 | "provider_id": 1234, 12 | "location_id": 1072404542, 13 | "callback_url": "https://google.com/", 14 | "tracking_support": false, 15 | "inventory_management": false, 16 | "admin_graphql_api_id": "gid://shopify/ApiFulfillmentService/1061774487", 17 | "permits_sku_sharing": false 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /fixtures/gift_card/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "gift_card": { 3 | "disabled_at": "2023-04-06T06:39:31-04:00", 4 | "template_suffix": null, 5 | "initial_value": "100.00", 6 | "balance": "100.00", 7 | "customer_id": null, 8 | "id": 1, 9 | "created_at": "2023-04-06T06:34:03-04:00", 10 | "updated_at": "2023-04-06T06:39:31-04:00", 11 | "currency": "USD", 12 | "line_item_id": null, 13 | "api_client_id": null, 14 | "user_id": null, 15 | "note": null, 16 | "expires_on": null, 17 | "last_characters": "0d0d", 18 | "order_id": null 19 | } 20 | } -------------------------------------------------------------------------------- /fixtures/gift_card/list.json: -------------------------------------------------------------------------------- 1 | { 2 | "gift_cards": [ 3 | { 4 | "id": 1, 5 | "balance": "25.00", 6 | "created_at": "2023-04-06T06:34:03-04:00", 7 | "updated_at": "2023-04-06T06:34:03-04:00", 8 | "currency": "USD", 9 | "initial_value": "50.00", 10 | "disabled_at": null, 11 | "line_item_id": null, 12 | "api_client_id": null, 13 | "user_id": null, 14 | "customer_id": null, 15 | "note": null, 16 | "expires_on": "2022-04-06", 17 | "template_suffix": null, 18 | "last_characters": "0e0e", 19 | "order_id": null 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /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 | "alt": "new alt tag content", 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 | } -------------------------------------------------------------------------------- /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 | "alt": "new alt tag content", 12 | "src": "https:\/\/cdn.shopify.com\/s\/files\/1\/0006\/9093\/3842\/products\/ipod-nano.png?v=1500937783", 13 | "variant_ids": [ 14 | 808950810, 15 | 808950811 16 | ] 17 | }, 18 | { 19 | "id": 2, 20 | "product_id": 1, 21 | "position": 2, 22 | "created_at": "2017-07-24T19:09:43-04:00", 23 | "updated_at": "2017-07-24T19:09:43-04:00", 24 | "width": 123, 25 | "height": 456, 26 | "alt": "new alt tag content 2", 27 | "src": "https:\/\/cdn.shopify.com\/s\/files\/1\/0006\/9093\/3842\/products\/ipod-nano-2.png?v=1500937783", 28 | "variant_ids": [ 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /fixtures/inventory_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "inventory_item": { 3 | "id": 808950810, 4 | "sku": "new sku", 5 | "created_at": "2018-10-29T06:05:58-04:00", 6 | "updated_at": "2018-10-29T06:05:58-04:00", 7 | "cost": "25.00", 8 | "tracked": true, 9 | "admin_graphql_api_id": "gid://shopify/InventoryItem/808950810", 10 | "country_code_of_origin": "US", 11 | "country_harmonized_system_codes": [ 12 | { 13 | "harmonized_system_code": "8471.70.40.35", 14 | "country_code": "US" 15 | }, 16 | { 17 | "harmonized_system_code": "8471.70.50.35", 18 | "country_code": "CA" 19 | } 20 | ], 21 | "harmonized_system_code": "8471.70.40.35", 22 | "province_code_of_origin": "ON" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/inventory_items.json: -------------------------------------------------------------------------------- 1 | { 2 | "inventory_items": [ 3 | { 4 | "id": 39072856, 5 | "sku": "IPOD2008GREEN", 6 | "created_at": "2018-10-29T06:05:58-04:00", 7 | "updated_at": "2018-10-29T06:05:58-04:00", 8 | "cost": "25.00", 9 | "tracked": true, 10 | "admin_graphql_api_id": "gid://shopify/InventoryItem/39072856" 11 | }, 12 | { 13 | "id": 457924702, 14 | "sku": "IPOD2008BLACK", 15 | "created_at": "2018-10-29T06:05:58-04:00", 16 | "updated_at": "2018-10-29T06:05:58-04:00", 17 | "cost": "25.00", 18 | "tracked": true, 19 | "admin_graphql_api_id": "gid://shopify/InventoryItem/457924702" 20 | }, 21 | { 22 | "id": 808950810, 23 | "sku": "IPOD2008PINK", 24 | "created_at": "2018-10-29T06:05:58-04:00", 25 | "updated_at": "2018-10-29T06:05:58-04:00", 26 | "cost": "25.00", 27 | "tracked": true, 28 | "admin_graphql_api_id": "gid://shopify/InventoryItem/808950810" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /fixtures/inventory_level.json: -------------------------------------------------------------------------------- 1 | { 2 | "inventory_level": { 3 | "inventory_item_id": 808950810, 4 | "location_id": 905684977, 5 | "available": 6, 6 | "updated_at": "2020-04-06T10:51:56-04:00", 7 | "admin_graphql_api_id": "gid://shopify/InventoryLevel/905684977?inventory_item_id=808950810" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/inventory_levels.json: -------------------------------------------------------------------------------- 1 | { 2 | "inventory_levels": [ 3 | { 4 | "inventory_item_id": 808950810, 5 | "location_id": 487838322, 6 | "available": 9, 7 | "updated_at": "2020-04-06T10:51:36-04:00", 8 | "admin_graphql_api_id": "gid://shopify/InventoryLevel/690933842?inventory_item_id=808950810" 9 | }, 10 | { 11 | "inventory_item_id": 39072856, 12 | "location_id": 487838322, 13 | "available": 27, 14 | "updated_at": "2020-04-06T10:51:36-04:00", 15 | "admin_graphql_api_id": "gid://shopify/InventoryLevel/690933842?inventory_item_id=39072856" 16 | }, 17 | { 18 | "inventory_item_id": 808950810, 19 | "location_id": 905684977, 20 | "available": 1, 21 | "updated_at": "2020-04-06T10:51:36-04:00", 22 | "admin_graphql_api_id": "gid://shopify/InventoryLevel/905684977?inventory_item_id=808950810" 23 | }, 24 | { 25 | "inventory_item_id": 39072856, 26 | "location_id": 905684977, 27 | "available": 3, 28 | "updated_at": "2020-04-06T10:51:36-04:00", 29 | "admin_graphql_api_id": "gid://shopify/InventoryLevel/905684977?inventory_item_id=39072856" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /fixtures/invoice.json: -------------------------------------------------------------------------------- 1 | { 2 | "draft_order_invoice": { 3 | "to": "first@example.com", 4 | "from": "steve@apple.com", 5 | "subject": "Apple Computer Invoice", 6 | "custom_message": "Thank you for ordering!", 7 | "bcc": [ 8 | "steve@apple.com" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /fixtures/location.json: -------------------------------------------------------------------------------- 1 | { 2 | "location": { 3 | "id": 4688969785, 4 | "name": "Bajkowa", 5 | "address1": "Bajkowa", 6 | "address2": "", 7 | "city": "Olsztyn", 8 | "zip": "10-001", 9 | "province": null, 10 | "country": "PL", 11 | "phone": "12312312", 12 | "created_at": "2018-02-19T16:18:59-05:00", 13 | "updated_at": "2018-02-19T16:19:00-05:00", 14 | "country_code": "PL", 15 | "country_name": "Poland", 16 | "province_code": null, 17 | "legacy": false, 18 | "active": true, 19 | "admin_graphql_api_id": "gid:\/\/shopify\/Location\/4688969785" 20 | } 21 | } -------------------------------------------------------------------------------- /fixtures/locations.json: -------------------------------------------------------------------------------- 1 | { 2 | "locations": [ 3 | { 4 | "id": 4688969785, 5 | "name": "Bajkowa", 6 | "address1": "Bajkowa", 7 | "address2": "", 8 | "city": "Olsztyn", 9 | "zip": "10-001", 10 | "province": null, 11 | "country": "PL", 12 | "phone": "12312312", 13 | "created_at": "2018-02-19T16:18:59-05:00", 14 | "updated_at": "2018-02-19T16:19:00-05:00", 15 | "country_code": "PL", 16 | "country_name": "Poland", 17 | "province_code": null, 18 | "legacy": false, 19 | "active": true, 20 | "admin_graphql_api_id": "gid:\/\/shopify\/Location\/4688969785" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /fixtures/metafield.json: -------------------------------------------------------------------------------- 1 | { 2 | "metafield": { 3 | "id": 1, 4 | "namespace": "affiliates", 5 | "key": "app_key", 6 | "value": "app_value", 7 | "type": "single_line_text_field", 8 | "description": "some amaaazing app's value", 9 | "owner_id": 1, 10 | "created_at": "2016-01-01T00:00:00Z", 11 | "updated_at": "2016-01-02T00:00:00Z", 12 | "owner_resource": "shop", 13 | "admin_graphql_api_id": "gid://shopify/Metafield/1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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","current_total_price":"9.50","subtotal_price":"0.00","total_weight":0,"total_tip_received": "0.00", "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_risk.json: -------------------------------------------------------------------------------- 1 | { 2 | "risk": { 3 | "id": 284138680, 4 | "order_id": 450789469, 5 | "checkout_id": 0, 6 | "source": "External", 7 | "score": "1.0", 8 | "recommendation": "cancel", 9 | "display": true, 10 | "cause_cancel": true, 11 | "message": "This order was placed from a proxy IP", 12 | "merchant_message": "This order was placed from a proxy IP" 13 | } 14 | } -------------------------------------------------------------------------------- /fixtures/order_risks.json: -------------------------------------------------------------------------------- 1 | { 2 | "risks": [ 3 | { 4 | "id": 284138680, 5 | "order_id": 450789469, 6 | "checkout_id": 0, 7 | "source": "External", 8 | "score": "1.0", 9 | "recommendation": "cancel", 10 | "display": true, 11 | "cause_cancel": true, 12 | "message": "This order was placed from a proxy IP", 13 | "merchant_message": "This order was placed from a proxy IP" 14 | }, 15 | { 16 | "id": 1029151489, 17 | "order_id": 450789469, 18 | "checkout_id": 901414060, 19 | "source": "External", 20 | "score": "1.0", 21 | "recommendation": "cancel", 22 | "display": true, 23 | "cause_cancel": true, 24 | "message": "This order came from an anonymous proxy", 25 | "merchant_message": "This order came from an anonymous proxy" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /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","current_total_price":"9.50","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/orderlineitems/properties_empty_object.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": {} 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/orderlineitems/properties_invalid0.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/orderlineitems/properties_invalid1.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "name": 2 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /fixtures/orderlineitems/properties_invalid2.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": 2 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/orderlineitems/properties_object.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "name": "property 1", 4 | "value": 3 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/orderlineitems/properties_ommited.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /fixtures/orderlineitems/valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "fulfillable_quantity": 1, 3 | "fulfillment_service": "manual", 4 | "fulfillment_status": "partial", 5 | "gift_card": true, 6 | "grams": 100, 7 | "id": 254721536, 8 | "name": "Soda", 9 | "pre_tax_price": "9.00", 10 | "price": "12.34", 11 | "product_exists": true, 12 | "product_id": 111475476, 13 | "properties": [ 14 | { 15 | "name": "note 1", 16 | "value": "one" 17 | }, 18 | { 19 | "name": "note 2", 20 | "value": 2 21 | } 22 | ], 23 | "quantity": 1, 24 | "requires_shipping": true, 25 | "sku": "sku-123", 26 | "tax_lines": [ 27 | { 28 | "price": 13.50, 29 | "rate": 0.06, 30 | "title": "State tax" 31 | }, 32 | { 33 | "price": 12.50, 34 | "rate": 0.05, 35 | "title": "Federal tax" 36 | } 37 | ], 38 | "taxable": true, 39 | "title": "Soda Title", 40 | "total_discount": "1.23", 41 | "variant_id": 1234, 42 | "variant_inventory_management": "shopify", 43 | "variant_title": "Test Variant", 44 | "vendor": "Test Vendor", 45 | "origin_location": { 46 | "id": 123, 47 | "address1": "100 some street", 48 | "address2": "", 49 | "city": "Winnipeg", 50 | "company": "Acme Corporation", 51 | "country": "Canada", 52 | "country_code": "CA", 53 | "first_name": "Bob", 54 | "last_name": "Smith", 55 | "latitude": 49.811550, 56 | "longitude": -97.189480, 57 | "name": "test address", 58 | "phone": "8675309", 59 | "province": "Manitoba", 60 | "province_code": "MB", 61 | "zip": "R3Y 0L6" 62 | }, 63 | "destination_location": { 64 | "id": 124, 65 | "address1": "200 some street", 66 | "address2": "", 67 | "city": "Winnipeg", 68 | "company": "Acme Corporation", 69 | "country": "Canada", 70 | "country_code": "CA", 71 | "first_name": "Bob", 72 | "last_name": "Smith", 73 | "latitude": 49.811550, 74 | "longitude": -97.189480, 75 | "name": "test address", 76 | "phone": "8675309", 77 | "province": "Manitoba", 78 | "province_code": "MB", 79 | "zip": "R3Y 0L6" 80 | }, 81 | "applied_discount": { 82 | "title": "test discount", 83 | "description": "my test discount", 84 | "value": "0.05", 85 | "value_type": "percent", 86 | "amount": "25.00" 87 | }, 88 | "discount_allocations": [ 89 | { 90 | "amount": "5.5", 91 | "amount_set": { 92 | "shop_money": { 93 | "amount": "5.5", 94 | "currency_code": "EUR" 95 | }, 96 | "presentment_money": { 97 | "amount": "5.5", 98 | "currency_code": "EUR" 99 | } 100 | } 101 | } 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /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","current_total_price":"9.50","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/payments_transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "transaction": { 3 | "id": 699519475, 4 | "type": "debit", 5 | "test": false, 6 | "payout_id": 623721858, 7 | "payout_status": "paid", 8 | "currency": "USD", 9 | "amount": "-50.00", 10 | "fee": "0.00", 11 | "net": "-50.00", 12 | "source_id": 460709370, 13 | "source_type": "adjustment", 14 | "source_order_id": 0, 15 | "source_order_transaction_id": 0, 16 | "processed_at": "2013-11-01" 17 | } 18 | } -------------------------------------------------------------------------------- /fixtures/payments_transactions.json: -------------------------------------------------------------------------------- 1 | { 2 | "transactions": [ 3 | { 4 | "id": 699519475, 5 | "type": "debit", 6 | "test": false, 7 | "payout_id": 623721858, 8 | "payout_status": "paid", 9 | "currency": "USD", 10 | "amount": "-50.00", 11 | "fee": "0.00", 12 | "net": "-50.00", 13 | "source_id": 460709370, 14 | "source_type": "adjustment", 15 | "source_order_id": null, 16 | "source_order_transaction_id": null, 17 | "processed_at": "2013-11-01" 18 | }, 19 | { 20 | "id": 77412310, 21 | "type": "credit", 22 | "test": false, 23 | "payout_id": 623721858, 24 | "payout_status": "paid", 25 | "currency": "USD", 26 | "amount": "50.00", 27 | "fee": "0.00", 28 | "net": "50.00", 29 | "source_id": 374511569, 30 | "source_type": "Payments::Balance::AdjustmentReversal", 31 | "source_order_id": null, 32 | "source_order_transaction_id": null, 33 | "processed_at": "2013-11-01" 34 | }, 35 | { 36 | "id": 1006917261, 37 | "type": "refund", 38 | "test": false, 39 | "payout_id": 623721858, 40 | "payout_status": "paid", 41 | "currency": "USD", 42 | "amount": "-3.45", 43 | "fee": "0.00", 44 | "net": "-3.45", 45 | "source_id": 1006917261, 46 | "source_type": "Payments::Refund", 47 | "source_order_id": 217130470, 48 | "source_order_transaction_id": 1006917261, 49 | "processed_at": "2013-11-01" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /fixtures/payout.json: -------------------------------------------------------------------------------- 1 | { 2 | "payout": { 3 | "id": 623721858, 4 | "status": "paid", 5 | "date": "2012-11-12", 6 | "currency": "USD", 7 | "amount": "41.90" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/payouts.json: -------------------------------------------------------------------------------- 1 | { 2 | "payouts": [ 3 | { 4 | "id": 854088011, 5 | "status": "scheduled", 6 | "date": "2013-11-01", 7 | "currency": "USD", 8 | "amount": "43.12" 9 | }, 10 | { 11 | "id": 512467833, 12 | "status": "failed", 13 | "date": "2013-11-01", 14 | "currency": "USD", 15 | "amount": "43.12" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /fixtures/payouts_filtered.json: -------------------------------------------------------------------------------- 1 | { 2 | "payouts": [ 3 | { 4 | "id": 854088011, 5 | "status": "scheduled", 6 | "date": "2013-11-01", 7 | "currency": "USD", 8 | "amount": "43.12" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /fixtures/price_rule/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "price_rule": { 3 | "id": 1, 4 | "value_type": "percentage", 5 | "value": "-10.0", 6 | "customer_selection": "all", 7 | "target_type": "line_item", 8 | "target_selection": "all", 9 | "allocation_method": "across", 10 | "allocation_limit": null, 11 | "once_per_customer": false, 12 | "usage_limit": null, 13 | "starts_at": "2020-07-05T22:40:32-04:00", 14 | "ends_at": null, 15 | "created_at": "2020-07-05T22:40:38-04:00", 16 | "updated_at": "2020-07-07T21:49:39-04:00", 17 | "entitled_product_ids": [], 18 | "entitled_variant_ids": [], 19 | "entitled_collection_ids": [], 20 | "entitled_country_ids": [], 21 | "prerequisite_product_ids": [], 22 | "prerequisite_variant_ids": [], 23 | "prerequisite_collection_ids": [], 24 | "prerequisite_saved_search_ids": [1123452345, 43535363636], 25 | "prerequisite_customer_ids": [384028349005, 3492039843], 26 | "prerequisite_subtotal_range": { 27 | "greater_than_or_equal_to": "40.0" 28 | }, 29 | "prerequisite_quantity_range": { 30 | "greater_than_or_equal_to": 2 31 | }, 32 | "prerequisite_shipping_price_range": { 33 | "less_than_or_equal_to": "10.0" 34 | }, 35 | "prerequisite_to_entitlement_quantity_ratio": { 36 | "prerequisite_quantity": 2, 37 | "entitled_quantity": 1 38 | }, 39 | "title": "some discount rule", 40 | "admin_graphql_api_id": "gid://shopify/PriceRule/1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /fixtures/price_rule/list.json: -------------------------------------------------------------------------------- 1 | { 2 | "price_rules": [ 3 | { 4 | "id": 1, 5 | "value_type": "percentage", 6 | "value": "-10.0", 7 | "customer_selection": "all", 8 | "target_type": "line_item", 9 | "target_selection": "all", 10 | "allocation_method": "across", 11 | "allocation_limit": null, 12 | "once_per_customer": false, 13 | "usage_limit": null, 14 | "starts_at": "2020-07-05T22:40:32-04:00", 15 | "ends_at": null, 16 | "created_at": "2020-07-05T22:40:38-04:00", 17 | "updated_at": "2020-07-07T21:49:39-04:00", 18 | "entitled_product_ids": [], 19 | "entitled_variant_ids": [], 20 | "entitled_collection_ids": [], 21 | "entitled_country_ids": [], 22 | "prerequisite_product_ids": [], 23 | "prerequisite_variant_ids": [], 24 | "prerequisite_collection_ids": [], 25 | "prerequisite_saved_search_ids": [], 26 | "prerequisite_customer_ids": [], 27 | "prerequisite_subtotal_range": null, 28 | "prerequisite_quantity_range": null, 29 | "prerequisite_shipping_price_range": null, 30 | "prerequisite_to_entitlement_quantity_ratio": { 31 | "prerequisite_quantity": null, 32 | "entitled_quantity": null 33 | }, 34 | "title": "some discount rule", 35 | "admin_graphql_api_id": "gid://shopify/PriceRule/1" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /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 | "admin_graphql_api_id": "gid://shopify/ProductVariant/1070325219" 42 | } 43 | ], 44 | "options": [ 45 | { 46 | "id": 1022828904, 47 | "product_id": 1071559748, 48 | "name": "Title", 49 | "position": 1, 50 | "values": [ 51 | "Default Title" 52 | ] 53 | } 54 | ], 55 | "images": [ 56 | ], 57 | "image": null, 58 | "admin_graphql_api_id": "gid://shopify/Product/1071559748" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /fixtures/product_listing.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_listing": { 3 | "product_id": 921728736, 4 | "created_at": "2020-07-09T13:45:19-04:00", 5 | "updated_at": "2020-07-09T13:45:19-04:00", 6 | "body_html": "

The iPod Touch has the iPhone's multi-touch interface, with a physical home button off the touch screen. The home screen has a list of buttons for the available applications.

", 7 | "handle": "ipod-touch", 8 | "product_type": "Cult Products", 9 | "title": "IPod Touch 8GB", 10 | "vendor": "Apple", 11 | "available": true, 12 | "tags": "", 13 | "published_at": "2017-08-31T20:00:00-04:00", 14 | "variants": [ 15 | { 16 | "id": 447654529, 17 | "title": "Black", 18 | "option_values": [ 19 | { 20 | "option_id": 891236591, 21 | "name": "Title", 22 | "value": "Black" 23 | } 24 | ], 25 | "price": "199.00", 26 | "formatted_price": "$199.00", 27 | "compare_at_price": null, 28 | "grams": 567, 29 | "requires_shipping": true, 30 | "sku": "IPOD2009BLACK", 31 | "barcode": "1234_black", 32 | "taxable": true, 33 | "position": 1, 34 | "available": true, 35 | "inventory_policy": "continue", 36 | "inventory_quantity": 13, 37 | "inventory_management": "shipwire-app", 38 | "fulfillment_service": "shipwire-app", 39 | "weight": 1.25, 40 | "weight_unit": "lb", 41 | "image_id": null, 42 | "created_at": "2020-07-09T13:45:19-04:00", 43 | "updated_at": "2020-07-09T13:45:19-04:00" 44 | } 45 | ], 46 | "images": [], 47 | "options": [ 48 | { 49 | "id": 891236591, 50 | "name": "Title", 51 | "product_id": 921728736, 52 | "position": 1, 53 | "values": [ 54 | "Black" 55 | ] 56 | } 57 | ] 58 | } 59 | } -------------------------------------------------------------------------------- /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/api/9999-99/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/api/9999-99/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/api/9999-99/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/api/9999-99/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/api/9999-99/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/api/9999-99/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/api/9999-99/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/api/9999-99/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/api/9999-99/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/shipping_zones.json: -------------------------------------------------------------------------------- 1 | { 2 | "shipping_zones": [ 3 | { 4 | "id": 1039932365, 5 | "name": "Some zone", 6 | "profile_id": "gid://shopify/DeliveryProfile/690933843", 7 | "location_group_id": "gid://shopify/DeliveryLocationGroup/955592433", 8 | "admin_graphql_api_id": "gid://shopify/DeliveryZone/972511399", 9 | "countries": [ 10 | { 11 | "id": 817138619, 12 | "name": "United States", 13 | "tax": 0.0, 14 | "code": "US", 15 | "tax_name": "Federal Tax", 16 | "shipping_zone_id": 1039932365, 17 | "provinces": [ 18 | { 19 | "id": 1069646656, 20 | "country_id": 817138619, 21 | "name": "New York", 22 | "code": "NY", 23 | "tax": 0.0, 24 | "tax_name": "State Tax", 25 | "tax_type": null, 26 | "tax_percentage": 0.0, 27 | "shipping_zone_id": 1039932365 28 | }, 29 | { 30 | "id": 1069646657, 31 | "country_id": 817138619, 32 | "name": "Ohio", 33 | "code": "OH", 34 | "tax": 0.0, 35 | "tax_name": "State Tax", 36 | "tax_type": null, 37 | "tax_percentage": 0.0, 38 | "shipping_zone_id": 1039932365 39 | } 40 | ] 41 | }, 42 | { 43 | "id": 879921427, 44 | "name": "Canada", 45 | "tax": 0.05, 46 | "code": "CA", 47 | "tax_name": "GST", 48 | "shipping_zone_id": 1039932365, 49 | "provinces": [ 50 | { 51 | "id": 1069646654, 52 | "country_id": 879921427, 53 | "name": "Ontario", 54 | "code": "ON", 55 | "tax": 0.13, 56 | "tax_name": "HST", 57 | "tax_type": "harmonized", 58 | "tax_percentage": 13.0, 59 | "shipping_zone_id": 1039932365 60 | }, 61 | { 62 | "id": 1069646655, 63 | "country_id": 879921427, 64 | "name": "Quebec", 65 | "code": "QC", 66 | "tax": 0.09975, 67 | "tax_name": "QST", 68 | "tax_type": null, 69 | "tax_percentage": 9.975, 70 | "shipping_zone_id": 1039932365 71 | } 72 | ] 73 | }, 74 | { 75 | "id": 1070231512, 76 | "name": "Yemen", 77 | "tax": 0.0, 78 | "code": "YE", 79 | "tax_name": "GST", 80 | "shipping_zone_id": 1039932365, 81 | "provinces": [] 82 | } 83 | ], 84 | "weight_based_shipping_rates": [ 85 | { 86 | "id": 882078075, 87 | "name": "Canada Air Shipping", 88 | "price": "25.00", 89 | "shipping_zone_id": 1039932365, 90 | "weight_low": 0.0, 91 | "weight_high": 11.0231 92 | } 93 | ], 94 | "price_based_shipping_rates": [ 95 | { 96 | "id": 882078074, 97 | "name": "$5 Shipping", 98 | "price": "5.05", 99 | "shipping_zone_id": 1039932365, 100 | "min_order_subtotal": "40.0", 101 | "max_order_subtotal": "100.0" 102 | } 103 | ], 104 | "carrier_shipping_rate_providers": [ 105 | { 106 | "id": 882078076, 107 | "carrier_service_id": 770241334, 108 | "flat_modifier": "0.0", 109 | "percent_modifier": 0, 110 | "service_filter": { "*": "+" }, 111 | "shipping_zone_id": 1039932365 112 | } 113 | ] 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /fixtures/shippinglines/requested_fulfillment_service_id_invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 123456789, 3 | "code": "INT.TP", 4 | "price": "4.00", 5 | "price_set": { 6 | "shop_money": { 7 | "amount": "4.00", 8 | "currency_code": "USD" 9 | }, 10 | "presentment_money": { 11 | "amount": "3.17", 12 | "currency_code": "EUR" 13 | } 14 | }, 15 | "discounted_price": "4.00", 16 | "discounted_price_set": { 17 | "shop_money": { 18 | "amount": "4.00", 19 | "currency_code": "USD" 20 | }, 21 | "presentment_money": { 22 | "amount": "3.17", 23 | "currency_code": "EUR" 24 | } 25 | }, 26 | "source": "canada_post", 27 | "title": "Small Packet International Air", 28 | "tax_lines": [ 29 | { 30 | "price": 13.50, 31 | "rate": 0.06, 32 | "title": "State tax" 33 | }, 34 | { 35 | "price": 12.50, 36 | "rate": 0.05, 37 | "title": "Federal tax" 38 | } 39 | ], 40 | "carrier_identifier": "third_party_carrier_identifier", 41 | "requested_fulfillment_service_id": 42 | } 43 | -------------------------------------------------------------------------------- /fixtures/shippinglines/requested_fulfillment_service_id_null.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 123456789, 3 | "code": "INT.TP", 4 | "price": "4.00", 5 | "price_set": { 6 | "shop_money": { 7 | "amount": "4.00", 8 | "currency_code": "USD" 9 | }, 10 | "presentment_money": { 11 | "amount": "3.17", 12 | "currency_code": "EUR" 13 | } 14 | }, 15 | "discounted_price": "4.00", 16 | "discounted_price_set": { 17 | "shop_money": { 18 | "amount": "4.00", 19 | "currency_code": "USD" 20 | }, 21 | "presentment_money": { 22 | "amount": "3.17", 23 | "currency_code": "EUR" 24 | } 25 | }, 26 | "source": "canada_post", 27 | "title": "Small Packet International Air", 28 | "tax_lines": [ 29 | { 30 | "price": 13.50, 31 | "rate": 0.06, 32 | "title": "State tax" 33 | }, 34 | { 35 | "price": 12.50, 36 | "rate": 0.05, 37 | "title": "Federal tax" 38 | } 39 | ], 40 | "carrier_identifier": "third_party_carrier_identifier", 41 | "requested_fulfillment_service_id": null, 42 | "handle": "test" 43 | } 44 | -------------------------------------------------------------------------------- /fixtures/shippinglines/requested_fulfillment_service_id_number.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 123456789, 3 | "code": "INT.TP", 4 | "price": "4.00", 5 | "price_set": { 6 | "shop_money": { 7 | "amount": "4.00", 8 | "currency_code": "USD" 9 | }, 10 | "presentment_money": { 11 | "amount": "3.17", 12 | "currency_code": "EUR" 13 | } 14 | }, 15 | "discounted_price": "4.00", 16 | "discounted_price_set": { 17 | "shop_money": { 18 | "amount": "4.00", 19 | "currency_code": "USD" 20 | }, 21 | "presentment_money": { 22 | "amount": "3.17", 23 | "currency_code": "EUR" 24 | } 25 | }, 26 | "source": "canada_post", 27 | "title": "Small Packet International Air", 28 | "tax_lines": [ 29 | { 30 | "price": 13.50, 31 | "rate": 0.06, 32 | "title": "State tax" 33 | }, 34 | { 35 | "price": 12.50, 36 | "rate": 0.05, 37 | "title": "Federal tax" 38 | } 39 | ], 40 | "carrier_identifier": "third_party_carrier_identifier", 41 | "requested_fulfillment_service_id": 123456, 42 | "handle": "test" 43 | } 44 | -------------------------------------------------------------------------------- /fixtures/shippinglines/valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 123456789, 3 | "code": "INT.TP", 4 | "price": "4.00", 5 | "price_set": { 6 | "shop_money": { 7 | "amount": "4.00", 8 | "currency_code": "USD" 9 | }, 10 | "presentment_money": { 11 | "amount": "3.17", 12 | "currency_code": "EUR" 13 | } 14 | }, 15 | "discounted_price": "4.00", 16 | "discounted_price_set": { 17 | "shop_money": { 18 | "amount": "4.00", 19 | "currency_code": "USD" 20 | }, 21 | "presentment_money": { 22 | "amount": "3.17", 23 | "currency_code": "EUR" 24 | } 25 | }, 26 | "source": "canada_post", 27 | "title": "Small Packet International Air", 28 | "tax_lines": [ 29 | { 30 | "price": 13.50, 31 | "rate": 0.06, 32 | "title": "State tax" 33 | }, 34 | { 35 | "price": 12.50, 36 | "rate": 0.05, 37 | "title": "Federal tax" 38 | } 39 | ], 40 | "carrier_identifier": "third_party_carrier_identifier", 41 | "requested_fulfillment_service_id": "third_party_fulfillment_service_id", 42 | "handle": "test" 43 | } 44 | -------------------------------------------------------------------------------- /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 | "pre_launch_enabled": false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/storefront_access_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "storefront_access_token": { 3 | "id": 755357713, 4 | "title": "API Client Extension", 5 | "access_token": "378d95641257a4ab3feff967ee234f4d", 6 | "access_scope": "unauthenticated_read_product_listings", 7 | "admin_graphql_api_id": "gid://shopify/StorefrontAccessToken/755357713", 8 | "created_at": "2016-06-01T14:10:44-00:00" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/storefront_access_tokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "storefront_access_tokens": [ 3 | { 4 | "id": 755357713, 5 | "title": "API Client Extension", 6 | "access_token": "378d95641257a4ab3feff967ee234f4d", 7 | "access_scope": "unauthenticated_read_product_listings", 8 | "admin_graphql_api_id": "gid://shopify/StorefrontAccessToken/755357713", 9 | "created_at": "2016-06-01T14:10:44-00:00" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /fixtures/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "id": 1, 4 | "name": "launchpad", 5 | "created_at": "2017-09-23T18:15:47-00:00", 6 | "updated_at": "2017-09-23T18:15:47-00:00", 7 | "role": "unpublished", 8 | "theme_store_id": 1234, 9 | "previewable": true, 10 | "processing": false, 11 | "admin_graphql_api_id": "gid://shopify/Theme/1234" 12 | } 13 | } -------------------------------------------------------------------------------- /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/usagecharge.json: -------------------------------------------------------------------------------- 1 | { 2 | "usage_charge": { 3 | "id": 1034618208, 4 | "description": "Super Mega Plan 1000 emails", 5 | "price": "1.00", 6 | "created_at": "2018-07-05T13:05:43-04:00", 7 | "billing_on": "2018-08-04", 8 | "balance_used": 11.0, 9 | "balance_remaining": 89.0, 10 | "risk_level": 0.08 11 | } 12 | } -------------------------------------------------------------------------------- /fixtures/usagecharges.json: -------------------------------------------------------------------------------- 1 | { 2 | "usage_charges": [ 3 | { 4 | "id": 1034618208, 5 | "description": "Super Mega Plan 1000 emails", 6 | "price": "1.00", 7 | "created_at": "2018-07-05T13:05:43-04:00", 8 | "billing_on": "2018-08-04", 9 | "balance_used": 11.0, 10 | "balance_remaining": 89.0, 11 | "risk_level": 0.08 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /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 | "inventory_item_id": 1, 14 | "option1": "Yellow", 15 | "option2": null, 16 | "option3": null, 17 | "created_at": "2017-10-18T12:22:36-04:00", 18 | "updated_at": "2017-10-18T12:22:36-04:00", 19 | "taxable": true, 20 | "barcode": null, 21 | "grams": 0, 22 | "image_id": null, 23 | "inventory_quantity": 1, 24 | "weight": 0, 25 | "weight_unit": "lb", 26 | "old_inventory_quantity": 1, 27 | "requires_shipping": true, 28 | "admin_graphql_api_id": "gid://shopify/ProductVariant/1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fixtures/variant_with_metafields.json: -------------------------------------------------------------------------------- 1 | { 2 | "variant": { 3 | "id": 2, 4 | "product_id": 1, 5 | "title": "Blue", 6 | "price": "2.00", 7 | "sku": "", 8 | "position": 2, 9 | "inventory_policy": "deny", 10 | "compare_at_price": null, 11 | "fulfillment_service": "manual", 12 | "inventory_management": null, 13 | "inventory_item_id": 1, 14 | "option1": "Yellow", 15 | "option2": null, 16 | "option3": null, 17 | "created_at": "2017-10-18T12:22:36-04:00", 18 | "updated_at": "2017-10-18T12:22:36-04:00", 19 | "taxable": true, 20 | "barcode": null, 21 | "grams": 0, 22 | "image_id": null, 23 | "inventory_quantity": 1, 24 | "weight": 0, 25 | "weight_unit": "lb", 26 | "old_inventory_quantity": 1, 27 | "requires_shipping": true, 28 | "admin_graphql_api_id": "gid://shopify/ProductVariant/1", 29 | "metafields": [ 30 | { 31 | "id": 721389482, 32 | "namespace": "affiliates", 33 | "key": "app_key", 34 | "value": "app_value", 35 | "value_type": "string", 36 | "description": "description", 37 | "owner_id": 690933842, 38 | "created_at": "2018-04-27T15:15:25-04:00", 39 | "updated_at": "2018-04-27T15:15:25-04:00", 40 | "owner_resource": "shop" 41 | } 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /fixtures/variant_with_taxcode.json: -------------------------------------------------------------------------------- 1 | { 2 | "variant": { 3 | "id": 1, 4 | "product_id": 1, 5 | "title": "Green", 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 | "inventory_item_id": 1, 14 | "option1": "Yellow", 15 | "option2": null, 16 | "option3": null, 17 | "created_at": "2017-10-18T12:22:36-04:00", 18 | "updated_at": "2017-10-18T12:22:36-04:00", 19 | "taxable": true, 20 | "tax_code": "P0000000", 21 | "barcode": null, 22 | "grams": 0, 23 | "image_id": null, 24 | "inventory_quantity": 1, 25 | "weight": 0, 26 | "weight_unit": "lb", 27 | "old_inventory_quantity": 1, 28 | "requires_shipping": true, 29 | "admin_graphql_api_id": "gid://shopify/ProductVariant/1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | "private_metafield_namespaces": [ 16 | "info-for", "my-app" 17 | ], 18 | "api_version": "2021-01" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 | "private_metafield_namespaces": [ 17 | "info-for", "my-app" 18 | ], 19 | "api_version": "2021-01" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /fulfillment_event.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | fulfillmentEventBasePath = "orders" 10 | ) 11 | 12 | // FulfillmentEventService is an interface for interfacing with the fulfillment event service 13 | // of the Shopify API. 14 | // https://shopify.dev/docs/api/admin-rest/latest/resources/fulfillmentevent 15 | type FulfillmentEventService interface { 16 | List(ctx context.Context, orderId uint64, fulfillmentId uint64) ([]FulfillmentEvent, error) 17 | Get(ctx context.Context, orderId uint64, fulfillmentId uint64, eventId uint64) (*FulfillmentEvent, error) 18 | Create(ctx context.Context, orderId uint64, fulfillmentId uint64, event FulfillmentEvent) (*FulfillmentEvent, error) 19 | Delete(ctx context.Context, orderId uint64, fulfillmentId uint64, eventId uint64) error 20 | } 21 | 22 | // FulfillmentEvent represents a Shopify fulfillment event. 23 | type FulfillmentEvent struct { 24 | Id uint64 `json:"id"` 25 | Address1 string `json:"address1"` 26 | City string `json:"city"` 27 | Country string `json:"country"` 28 | CreatedAt string `json:"created_at"` 29 | EstimatedDeliveryAt string `json:"estimated_delivery_at"` 30 | FulfillmentId uint64 `json:"fulfillment_id"` 31 | HappenedAt string `json:"happened_at"` 32 | Latitude float64 `json:"latitude"` 33 | Longitude float64 `json:"longitude"` 34 | Message string `json:"message"` 35 | OrderId uint64 `json:"order_id"` 36 | Province string `json:"province"` 37 | ShopId uint64 `json:"shop_id"` 38 | Status string `json:"status"` 39 | UpdatedAt string `json:"updated_at"` 40 | Zip string `json:"zip"` 41 | } 42 | 43 | type FulfillmentEventCreateRequest struct { 44 | Event *FulfillmentEvent `json:"event"` 45 | } 46 | 47 | type FulfillmentEventResource struct { 48 | FulfillmentEvent *FulfillmentEvent `json:"fulfillment_event,omitempty"` 49 | Event *FulfillmentEvent `json:"event,omitempty"` 50 | } 51 | 52 | type FulfillmentEventsResource struct { 53 | FulfillmentEvents []FulfillmentEvent `json:"fulfillment_events"` 54 | } 55 | 56 | // FulfillmentEventServiceOp handles communication with the fulfillment event related methods of the Shopify API. 57 | type FulfillmentEventServiceOp struct { 58 | client *Client 59 | } 60 | 61 | // List of all FulfillmentEvents for an order's fulfillment. The API returns the list under the 'fulfillment_events' key. 62 | func (s *FulfillmentEventServiceOp) List(ctx context.Context, orderId uint64, fulfillmentId uint64) ([]FulfillmentEvent, error) { 63 | path := fmt.Sprintf("%s/%d/fulfillments/%d/events.json", fulfillmentEventBasePath, orderId, fulfillmentId) 64 | resource := new(FulfillmentEventsResource) 65 | err := s.client.Get(ctx, path, resource, nil) 66 | return resource.FulfillmentEvents, err 67 | } 68 | 69 | // Get a single FulfillmentEvent. The API returns the event under the 'fulfillment_event' key. 70 | func (s *FulfillmentEventServiceOp) Get(ctx context.Context, orderId uint64, fulfillmentId uint64, eventId uint64) (*FulfillmentEvent, error) { 71 | path := fmt.Sprintf("%s/%d/fulfillments/%d/events/%d.json", fulfillmentEventBasePath, orderId, fulfillmentId, eventId) 72 | resource := new(FulfillmentEventResource) 73 | err := s.client.Get(ctx, path, resource, nil) 74 | return resource.FulfillmentEvent, err 75 | } 76 | 77 | // Create a new FulfillmentEvent 78 | func (s *FulfillmentEventServiceOp) Create(ctx context.Context, orderId uint64, fulfillmentId uint64, event FulfillmentEvent) (*FulfillmentEvent, error) { 79 | path := fmt.Sprintf("%s/%d/fulfillments/%d/events.json", fulfillmentEventBasePath, orderId, fulfillmentId) 80 | wrappedData := FulfillmentEventResource{Event: &event} 81 | resource := new(FulfillmentEventResource) 82 | err := s.client.Post(ctx, path, wrappedData, resource) 83 | return resource.FulfillmentEvent, err 84 | } 85 | 86 | // Delete an existing FulfillmentEvent 87 | func (s *FulfillmentEventServiceOp) Delete(ctx context.Context, orderId uint64, fulfillmentId uint64, eventId uint64) error { 88 | path := fmt.Sprintf("%s/%d/fulfillments/%d/events/%d.json", fulfillmentEventBasePath, orderId, fulfillmentId, eventId) 89 | return s.client.Delete(ctx, path) 90 | } 91 | -------------------------------------------------------------------------------- /fulfillment_request.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | fulfillmentRequestBasePath = "fulfillment_orders" 10 | ) 11 | 12 | // FulfillmentRequestService is an interface for interfacing with the fulfillment request endpoints of the Shopify API. 13 | // https://shopify.dev/docs/api/admin-rest/2023-10/resources/fulfillmentrequest 14 | type FulfillmentRequestService interface { 15 | Send(context.Context, uint64, FulfillmentRequest) (*FulfillmentOrder, error) 16 | Accept(context.Context, uint64, FulfillmentRequest) (*FulfillmentOrder, error) 17 | Reject(context.Context, uint64, FulfillmentRequest) (*FulfillmentOrder, error) 18 | } 19 | 20 | type FulfillmentRequest struct { 21 | Message string `json:"message,omitempty"` 22 | FulfillmentOrderLineItems []FulfillmentOrderLineItem `json:"fulfillment_order_line_items,omitempty"` 23 | Reason string `json:"reason,omitempty"` 24 | LineItems []FulfillmentRequestLineItem `json:"line_items,omitempty"` 25 | } 26 | 27 | type FulfillmentRequestOrderLineItem struct { 28 | Id uint64 `json:"id"` 29 | Quantity uint64 `json:"quantity"` 30 | } 31 | 32 | type FulfillmentRequestLineItem struct { 33 | FulfillmentOrderLineItemId uint64 `json:"fulfillment_order_line_item_id,omitempty"` 34 | Message string `json:"message,omitempty"` 35 | } 36 | 37 | type FulfillmentRequestResource struct { 38 | FulfillmentOrder *FulfillmentOrder `json:"fulfillment_order,omitempty"` 39 | FulfillmentRequest FulfillmentRequest `json:"fulfillment_request,omitempty"` 40 | OriginalFulfillmentOrder *FulfillmentOrder `json:"original_fulfillment_order,omitempty"` 41 | } 42 | 43 | // FulfillmentRequestServiceOp handles communication with the fulfillment request related methods of the Shopify API. 44 | type FulfillmentRequestServiceOp struct { 45 | client *Client 46 | } 47 | 48 | // Send sends a fulfillment request to the fulfillment service of a fulfillment order. 49 | func (s *FulfillmentRequestServiceOp) Send(ctx context.Context, fulfillmentOrderId uint64, request FulfillmentRequest) (*FulfillmentOrder, error) { 50 | path := fmt.Sprintf("%s/%d/fulfillment_request.json", fulfillmentRequestBasePath, fulfillmentOrderId) 51 | wrappedData := FulfillmentRequestResource{FulfillmentRequest: request} 52 | resource := new(FulfillmentRequestResource) 53 | err := s.client.Post(ctx, path, wrappedData, resource) 54 | return resource.OriginalFulfillmentOrder, err 55 | } 56 | 57 | // Accept accepts a fulfillment request sent to a fulfillment service for a fulfillment order. 58 | func (s *FulfillmentRequestServiceOp) Accept(ctx context.Context, fulfillmentOrderId uint64, request FulfillmentRequest) (*FulfillmentOrder, error) { 59 | path := fmt.Sprintf("%s/%d/fulfillment_request/accept.json", fulfillmentRequestBasePath, fulfillmentOrderId) 60 | wrappedData := map[string]interface{}{"fulfillment_request": request} 61 | resource := new(FulfillmentRequestResource) 62 | err := s.client.Post(ctx, path, wrappedData, resource) 63 | return resource.FulfillmentOrder, err 64 | } 65 | 66 | // Reject rejects a fulfillment request sent to a fulfillment service for a fulfillment order. 67 | func (s *FulfillmentRequestServiceOp) Reject(ctx context.Context, fulfillmentOrderId uint64, request FulfillmentRequest) (*FulfillmentOrder, error) { 68 | path := fmt.Sprintf("%s/%d/fulfillment_request/reject.json", fulfillmentRequestBasePath, fulfillmentOrderId) 69 | wrappedData := map[string]interface{}{"fulfillment_request": request} 70 | resource := new(FulfillmentRequestResource) 71 | err := s.client.Post(ctx, path, wrappedData, resource) 72 | return resource.FulfillmentOrder, err 73 | } 74 | -------------------------------------------------------------------------------- /gift_card_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/jarcoal/httpmock" 9 | ) 10 | 11 | func TestGiftCardGet(t *testing.T) { 12 | setup() 13 | defer teardown() 14 | 15 | httpmock.RegisterResponder( 16 | "GET", 17 | fmt.Sprintf("https://fooshop.myshopify.com/%s/gift_cards/1.json", client.pathPrefix), 18 | httpmock.NewBytesResponder( 19 | 200, 20 | loadFixture("gift_card/get.json"), 21 | ), 22 | ) 23 | 24 | giftCard, err := client.GiftCard.Get(context.Background(), 1) 25 | if err != nil { 26 | t.Errorf("GiftCard.Get returned error: %v", err) 27 | } 28 | 29 | expected := GiftCard{Id: 1} 30 | if expected.Id != giftCard.Id { 31 | t.Errorf("GiftCard.Get returned %+v, expected %+v", giftCard, expected) 32 | } 33 | } 34 | 35 | func TestGiftCardList(t *testing.T) { 36 | setup() 37 | defer teardown() 38 | 39 | httpmock.RegisterResponder( 40 | "GET", 41 | fmt.Sprintf("https://fooshop.myshopify.com/%s/gift_cards.json", client.pathPrefix), 42 | httpmock.NewBytesResponder( 43 | 200, 44 | loadFixture("gift_card/list.json"), 45 | ), 46 | ) 47 | 48 | giftCard, err := client.GiftCard.List(context.Background()) 49 | if err != nil { 50 | t.Errorf("GiftCard.List returned error: %v", err) 51 | } 52 | 53 | expected := []GiftCard{{Id: 1}} 54 | if expected[0].Id != giftCard[0].Id { 55 | t.Errorf("GiftCard.List returned %+v, expected %+v", giftCard, expected) 56 | } 57 | } 58 | 59 | func TestGiftCardCreate(t *testing.T) { 60 | setup() 61 | defer teardown() 62 | 63 | httpmock.RegisterResponder( 64 | "POST", 65 | fmt.Sprintf("https://fooshop.myshopify.com/%s/gift_cards.json", client.pathPrefix), 66 | httpmock.NewBytesResponder( 67 | 200, 68 | loadFixture("gift_card/get.json"), 69 | ), 70 | ) 71 | 72 | giftCard, err := client.GiftCard.Create(context.Background(), GiftCard{}) 73 | if err != nil { 74 | t.Errorf("GiftCard.Create returned error: %v", err) 75 | } 76 | 77 | expected := GiftCard{Id: 1} 78 | if expected.Id != giftCard.Id { 79 | t.Errorf("GiftCard.Create returned %+v, expected %+v", giftCard, expected) 80 | } 81 | } 82 | 83 | func TestGiftCardUpdate(t *testing.T) { 84 | setup() 85 | defer teardown() 86 | 87 | httpmock.RegisterResponder( 88 | "PUT", 89 | fmt.Sprintf("https://fooshop.myshopify.com/%s/gift_cards/1.json", client.pathPrefix), 90 | httpmock.NewBytesResponder( 91 | 200, 92 | loadFixture("gift_card/get.json"), 93 | ), 94 | ) 95 | 96 | giftCard, err := client.GiftCard.Update(context.Background(), GiftCard{Id: 1}) 97 | if err != nil { 98 | t.Errorf("GiftCard.Update returned error: %v", err) 99 | } 100 | 101 | expected := GiftCard{Id: 1} 102 | if expected.Id != giftCard.Id { 103 | t.Errorf("GiftCard.Update returned %+v, expected %+v", giftCard, expected) 104 | } 105 | } 106 | 107 | func TestGiftCardDisable(t *testing.T) { 108 | setup() 109 | defer teardown() 110 | 111 | httpmock.RegisterResponder( 112 | "POST", 113 | fmt.Sprintf("https://fooshop.myshopify.com/%s/gift_cards/1/disable.json", client.pathPrefix), 114 | httpmock.NewBytesResponder( 115 | 200, 116 | loadFixture("gift_card/get.json"), 117 | ), 118 | ) 119 | 120 | giftCard, err := client.GiftCard.Disable(context.Background(), 1) 121 | if err != nil { 122 | t.Errorf("GiftCard.Disable returned error: %v", err) 123 | } 124 | 125 | expected := []GiftCard{{Id: 1}} 126 | if expected[0].Id != giftCard.Id { 127 | t.Errorf("GiftCard.Disable returned %+v, expected %+v", giftCard, expected) 128 | } 129 | } 130 | 131 | func TestGiftCardCount(t *testing.T) { 132 | setup() 133 | defer teardown() 134 | 135 | httpmock.RegisterResponder( 136 | "GET", 137 | fmt.Sprintf("https://fooshop.myshopify.com/%s/gift_cards/count.json", client.pathPrefix), 138 | httpmock.NewStringResponder( 139 | 200, 140 | `{"count": 5}`, 141 | ), 142 | ) 143 | 144 | cnt, err := client.GiftCard.Count(context.Background(), nil) 145 | if err != nil { 146 | t.Errorf("GiftCard.Count returned error: %v", err) 147 | } 148 | 149 | expected := 5 150 | if cnt != expected { 151 | t.Errorf("GiftCard.Count returned %d, expected %d", cnt, expected) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bold-commerce/go-shopify/v4 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/google/go-querystring v1.0.0 7 | github.com/jarcoal/httpmock v1.3.0 8 | github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 4 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 5 | github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= 6 | github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= 7 | github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= 8 | github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= 9 | github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114 h1:Pm6R878vxWWWR+Sa3ppsLce/Zq+JNTs6aVvRu13jv9A= 10 | github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 11 | -------------------------------------------------------------------------------- /inventory_item.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | const inventoryItemsBasePath = "inventory_items" 12 | 13 | // InventoryItemService is an interface for interacting with the 14 | // inventory items endpoints of the Shopify API 15 | // See https://shopify.dev/docs/api/admin-rest/latest/resources/inventoryitem 16 | type InventoryItemService interface { 17 | List(context.Context, interface{}) ([]InventoryItem, error) 18 | Get(context.Context, uint64, interface{}) (*InventoryItem, error) 19 | Update(context.Context, InventoryItem) (*InventoryItem, error) 20 | } 21 | 22 | // InventoryItemServiceOp is the default implementation of the InventoryItemService interface 23 | type InventoryItemServiceOp struct { 24 | client *Client 25 | } 26 | 27 | // CountryHarmonizedSystemCode represents a harmonized system code for a specific country 28 | type CountryHarmonizedSystemCode struct { 29 | HarmonizedSystemCode string `json:"harmonized_system_code"` 30 | CountryCode string `json:"country_code"` 31 | } 32 | 33 | // InventoryItem represents a Shopify inventory item 34 | type InventoryItem struct { 35 | Id uint64 `json:"id,omitempty"` 36 | SKU string `json:"sku,omitempty"` 37 | CreatedAt *time.Time `json:"created_at,omitempty"` 38 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 39 | Cost *decimal.Decimal `json:"cost,omitempty"` 40 | Tracked *bool `json:"tracked,omitempty"` 41 | AdminGraphqlApiId string `json:"admin_graphql_api_id,omitempty"` 42 | CountryCodeOfOrigin *string `json:"country_code_of_origin"` 43 | CountryHarmonizedSystemCodes []CountryHarmonizedSystemCode `json:"country_harmonized_system_codes"` 44 | HarmonizedSystemCode *string `json:"harmonized_system_code"` 45 | ProvinceCodeOfOrigin *string `json:"province_code_of_origin"` 46 | } 47 | 48 | // InventoryItemResource is used for handling single item requests and responses 49 | type InventoryItemResource struct { 50 | InventoryItem *InventoryItem `json:"inventory_item"` 51 | } 52 | 53 | // InventoryItemsResource is used for handling multiple item responsees 54 | type InventoryItemsResource struct { 55 | InventoryItems []InventoryItem `json:"inventory_items"` 56 | } 57 | 58 | // List inventory items 59 | func (s *InventoryItemServiceOp) List(ctx context.Context, options interface{}) ([]InventoryItem, error) { 60 | path := fmt.Sprintf("%s.json", inventoryItemsBasePath) 61 | resource := new(InventoryItemsResource) 62 | err := s.client.Get(ctx, path, resource, options) 63 | return resource.InventoryItems, err 64 | } 65 | 66 | // Get a inventory item 67 | func (s *InventoryItemServiceOp) Get(ctx context.Context, id uint64, options interface{}) (*InventoryItem, error) { 68 | path := fmt.Sprintf("%s/%d.json", inventoryItemsBasePath, id) 69 | resource := new(InventoryItemResource) 70 | err := s.client.Get(ctx, path, resource, options) 71 | return resource.InventoryItem, err 72 | } 73 | 74 | // Update a inventory item 75 | func (s *InventoryItemServiceOp) Update(ctx context.Context, item InventoryItem) (*InventoryItem, error) { 76 | path := fmt.Sprintf("%s/%d.json", inventoryItemsBasePath, item.Id) 77 | wrappedData := InventoryItemResource{InventoryItem: &item} 78 | resource := new(InventoryItemResource) 79 | err := s.client.Put(ctx, path, wrappedData, resource) 80 | return resource.InventoryItem, err 81 | } 82 | -------------------------------------------------------------------------------- /inventory_level.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const inventoryLevelsBasePath = "inventory_levels" 10 | 11 | // InventoryLevelService is an interface for interacting with the 12 | // inventory items endpoints of the Shopify API 13 | // See https://shopify.dev/docs/api/admin-rest/latest/resources/inventorylevel 14 | type InventoryLevelService interface { 15 | List(context.Context, interface{}) ([]InventoryLevel, error) 16 | Adjust(context.Context, interface{}) (*InventoryLevel, error) 17 | Delete(context.Context, uint64, uint64) error 18 | Connect(context.Context, InventoryLevel) (*InventoryLevel, error) 19 | Set(context.Context, InventoryLevel) (*InventoryLevel, error) 20 | } 21 | 22 | // InventoryLevelServiceOp is the default implementation of the InventoryLevelService interface 23 | type InventoryLevelServiceOp struct { 24 | client *Client 25 | } 26 | 27 | // InventoryLevel represents a Shopify inventory level 28 | type InventoryLevel struct { 29 | InventoryItemId uint64 `json:"inventory_item_id,omitempty"` 30 | LocationId uint64 `json:"location_id,omitempty"` 31 | Available int `json:"available"` 32 | CreatedAt *time.Time `json:"created_at,omitempty"` 33 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 34 | AdminGraphqlApiId string `json:"admin_graphql_api_id,omitempty"` 35 | } 36 | 37 | // InventoryLevelResource is used for handling single level requests and responses 38 | type InventoryLevelResource struct { 39 | InventoryLevel *InventoryLevel `json:"inventory_level"` 40 | } 41 | 42 | // InventoryLevelsResource is used for handling multiple item responsees 43 | type InventoryLevelsResource struct { 44 | InventoryLevels []InventoryLevel `json:"inventory_levels"` 45 | } 46 | 47 | // InventoryLevelListOptions is used for get list 48 | type InventoryLevelListOptions struct { 49 | InventoryItemIds []uint64 `url:"inventory_item_ids,omitempty,comma"` 50 | LocationIds []uint64 `url:"location_ids,omitempty,comma"` 51 | Limit int `url:"limit,omitempty"` 52 | UpdatedAtMin time.Time `url:"updated_at_min,omitempty"` 53 | } 54 | 55 | // InventoryLevelAdjustOptions is used for Adjust inventory levels 56 | type InventoryLevelAdjustOptions struct { 57 | InventoryItemId uint64 `json:"inventory_item_id"` 58 | LocationId uint64 `json:"location_id"` 59 | Adjust int `json:"available_adjustment"` 60 | } 61 | 62 | // List inventory levels 63 | func (s *InventoryLevelServiceOp) List(ctx context.Context, options interface{}) ([]InventoryLevel, error) { 64 | path := fmt.Sprintf("%s.json", inventoryLevelsBasePath) 65 | resource := new(InventoryLevelsResource) 66 | err := s.client.Get(ctx, path, resource, options) 67 | return resource.InventoryLevels, err 68 | } 69 | 70 | // Delete an inventory level 71 | func (s *InventoryLevelServiceOp) Delete(ctx context.Context, itemId, locationId uint64) error { 72 | path := fmt.Sprintf("%s.json?inventory_item_id=%v&location_id=%v", 73 | inventoryLevelsBasePath, itemId, locationId) 74 | return s.client.Delete(ctx, path) 75 | } 76 | 77 | // Connect an inventory level 78 | func (s *InventoryLevelServiceOp) Connect(ctx context.Context, level InventoryLevel) (*InventoryLevel, error) { 79 | return s.post(ctx, fmt.Sprintf("%s/connect.json", inventoryLevelsBasePath), level) 80 | } 81 | 82 | // Set an inventory level 83 | func (s *InventoryLevelServiceOp) Set(ctx context.Context, level InventoryLevel) (*InventoryLevel, error) { 84 | return s.post(ctx, fmt.Sprintf("%s/set.json", inventoryLevelsBasePath), level) 85 | } 86 | 87 | // Adjust the inventory level of an inventory item at a single location 88 | func (s *InventoryLevelServiceOp) Adjust(ctx context.Context, options interface{}) (*InventoryLevel, error) { 89 | return s.post(ctx, fmt.Sprintf("%s/adjust.json", inventoryLevelsBasePath), options) 90 | } 91 | 92 | func (s *InventoryLevelServiceOp) post(ctx context.Context, path string, options interface{}) (*InventoryLevel, error) { 93 | resource := new(InventoryLevelResource) 94 | err := s.client.Post(ctx, path, options, resource) 95 | return resource.InventoryLevel, err 96 | } 97 | -------------------------------------------------------------------------------- /location_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/jarcoal/httpmock" 11 | ) 12 | 13 | func TestLocationServiceOp_List(t *testing.T) { 14 | setup() 15 | defer teardown() 16 | 17 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/locations.json", client.pathPrefix), 18 | httpmock.NewBytesResponder(200, loadFixture("locations.json"))) 19 | 20 | products, err := client.Location.List(context.Background(), nil) 21 | if err != nil { 22 | t.Errorf("Location.List returned error: %v", err) 23 | } 24 | 25 | created, _ := time.Parse(time.RFC3339, "2018-02-19T16:18:59-05:00") 26 | updated, _ := time.Parse(time.RFC3339, "2018-02-19T16:19:00-05:00") 27 | 28 | expected := []Location{{ 29 | Id: 4688969785, 30 | Name: "Bajkowa", 31 | Address1: "Bajkowa", 32 | Address2: "", 33 | City: "Olsztyn", 34 | Zip: "10-001", 35 | Country: "PL", 36 | Phone: "12312312", 37 | CreatedAt: created, 38 | UpdatedAt: updated, 39 | CountryCode: "PL", 40 | CountryName: "Poland", 41 | Legacy: false, 42 | Active: true, 43 | AdminGraphqlApiId: "gid://shopify/Location/4688969785", 44 | }} 45 | 46 | if !reflect.DeepEqual(products, expected) { 47 | t.Errorf("Location.List returned %+v, expected %+v", products, expected) 48 | } 49 | } 50 | 51 | func TestLocationServiceOp_Get(t *testing.T) { 52 | setup() 53 | defer teardown() 54 | 55 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/locations/4688969785.json", client.pathPrefix), 56 | httpmock.NewBytesResponder(200, loadFixture("location.json"))) 57 | 58 | product, err := client.Location.Get(context.Background(), 4688969785, nil) 59 | if err != nil { 60 | t.Errorf("Location.Get returned error: %v", err) 61 | } 62 | 63 | created, _ := time.Parse(time.RFC3339, "2018-02-19T16:18:59-05:00") 64 | updated, _ := time.Parse(time.RFC3339, "2018-02-19T16:19:00-05:00") 65 | 66 | expected := &Location{ 67 | Id: 4688969785, 68 | Name: "Bajkowa", 69 | Address1: "Bajkowa", 70 | Address2: "", 71 | City: "Olsztyn", 72 | Zip: "10-001", 73 | Country: "PL", 74 | Phone: "12312312", 75 | CreatedAt: created, 76 | UpdatedAt: updated, 77 | CountryCode: "PL", 78 | CountryName: "Poland", 79 | Legacy: false, 80 | Active: true, 81 | AdminGraphqlApiId: "gid://shopify/Location/4688969785", 82 | } 83 | 84 | if !reflect.DeepEqual(product, expected) { 85 | t.Errorf("Location.Get returned %+v, expected %+v", product, expected) 86 | } 87 | } 88 | 89 | func TestLocationServiceOp_Count(t *testing.T) { 90 | setup() 91 | defer teardown() 92 | 93 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/locations/count.json", client.pathPrefix), 94 | httpmock.NewStringResponder(200, `{"count": 3}`)) 95 | 96 | cnt, err := client.Location.Count(context.Background(), nil) 97 | if err != nil { 98 | t.Errorf("Location.Count returned error: %v", err) 99 | } 100 | 101 | expected := 3 102 | if cnt != expected { 103 | t.Errorf("Location.Count returned %d, expected %d", cnt, expected) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // idea from https://github.com/stripe/stripe-go/blob/master/log.go 10 | 11 | const ( 12 | LevelError = iota + 1 13 | LevelWarn 14 | LevelInfo 15 | LevelDebug 16 | ) 17 | 18 | type LeveledLoggerInterface interface { 19 | Debugf(format string, v ...interface{}) 20 | Errorf(format string, v ...interface{}) 21 | Infof(format string, v ...interface{}) 22 | Warnf(format string, v ...interface{}) 23 | } 24 | 25 | // It prints warnings and errors to `os.Stderr` and other messages to 26 | // `os.Stdout`. 27 | type LeveledLogger struct { 28 | // Level is the minimum logging level that will be emitted by this logger. 29 | // 30 | // For example, a Level set to LevelWarn will emit warnings and errors, but 31 | // not informational or debug messages. 32 | // 33 | // Always set this with a constant like LevelWarn because the individual 34 | // values are not guaranteed to be stable. 35 | Level int 36 | 37 | // Internal testing use only. 38 | stderrOverride io.Writer 39 | stdoutOverride io.Writer 40 | } 41 | 42 | // Debugf logs a debug message using Printf conventions. 43 | func (l *LeveledLogger) Debugf(format string, v ...interface{}) { 44 | if l.Level >= LevelDebug { 45 | fmt.Fprintf(l.stdout(), "[DEBUG] "+format+"\n", v...) 46 | } 47 | } 48 | 49 | // Errorf logs a warning message using Printf conventions. 50 | func (l *LeveledLogger) Errorf(format string, v ...interface{}) { 51 | // Infof logs a debug message using Printf conventions. 52 | if l.Level >= LevelError { 53 | fmt.Fprintf(l.stderr(), "[ERROR] "+format+"\n", v...) 54 | } 55 | } 56 | 57 | // Infof logs an informational message using Printf conventions. 58 | func (l *LeveledLogger) Infof(format string, v ...interface{}) { 59 | if l.Level >= LevelInfo { 60 | fmt.Fprintf(l.stdout(), "[INFO] "+format+"\n", v...) 61 | } 62 | } 63 | 64 | // Warnf logs a warning message using Printf conventions. 65 | func (l *LeveledLogger) Warnf(format string, v ...interface{}) { 66 | if l.Level >= LevelWarn { 67 | fmt.Fprintf(l.stderr(), "[WARN] "+format+"\n", v...) 68 | } 69 | } 70 | 71 | func (l *LeveledLogger) stderr() io.Writer { 72 | if l.stderrOverride != nil { 73 | return l.stderrOverride 74 | } 75 | 76 | return os.Stderr 77 | } 78 | 79 | func (l *LeveledLogger) stdout() io.Writer { 80 | if l.stdoutOverride != nil { 81 | return l.stdoutOverride 82 | } 83 | 84 | return os.Stdout 85 | } 86 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestLeveledLogger(t *testing.T) { 14 | tests := []struct { 15 | level int 16 | input string 17 | stdout string 18 | stderr string 19 | }{ 20 | { 21 | level: LevelError, 22 | input: "log", 23 | stderr: "[ERROR] error log\n", 24 | stdout: "", 25 | }, 26 | { 27 | level: LevelWarn, 28 | input: "log", 29 | stderr: "[ERROR] error log\n[WARN] warn log\n", 30 | stdout: "", 31 | }, 32 | { 33 | level: LevelInfo, 34 | input: "log", 35 | stderr: "[ERROR] error log\n[WARN] warn log\n", 36 | stdout: "[INFO] info log\n", 37 | }, 38 | { 39 | level: LevelDebug, 40 | input: "log", 41 | stderr: "[ERROR] error log\n[WARN] warn log\n", 42 | stdout: "[INFO] info log\n[DEBUG] debug log\n", 43 | }, 44 | } 45 | 46 | for _, test := range tests { 47 | err := &bytes.Buffer{} 48 | out := &bytes.Buffer{} 49 | log := &LeveledLogger{Level: test.level, stderrOverride: err, stdoutOverride: out} 50 | 51 | log.Errorf("error %s", test.input) 52 | log.Warnf("warn %s", test.input) 53 | log.Infof("info %s", test.input) 54 | log.Debugf("debug %s", test.input) 55 | 56 | stdout := out.String() 57 | stderr := err.String() 58 | 59 | if stdout != test.stdout { 60 | t.Errorf("leveled logger %d expected stdout \"%s\" received \"%s\"", test.level, test.stdout, stdout) 61 | } 62 | if stderr != test.stderr { 63 | t.Errorf("leveled logger %d expected stderr \"%s\" received \"%s\"", test.level, test.stderr, stderr) 64 | } 65 | } 66 | 67 | log := &LeveledLogger{Level: LevelDebug} 68 | if log.stderr() != os.Stderr { 69 | t.Errorf("leveled logger with no stderr override expects os.Stderr") 70 | } 71 | if log.stdout() != os.Stdout { 72 | t.Errorf("leveled logger with no stdout override expects os.Stdout") 73 | } 74 | 75 | } 76 | 77 | func TestDoGetHeadersDebug(t *testing.T) { 78 | err := &bytes.Buffer{} 79 | out := &bytes.Buffer{} 80 | logger := &LeveledLogger{Level: LevelDebug, stderrOverride: err, stdoutOverride: out} 81 | 82 | reqExpected := "[DEBUG] GET: //http:%2F%2Ftest.com/foo/1\n[DEBUG] SENT: request body\n" 83 | resExpected := "[DEBUG] Shopify X-Request-Id: 00000000-0000-0000-0000-000000000000\n[DEBUG] RECV 200: OK\n[DEBUG] RESP: response body\n" 84 | 85 | client := MustNewClient(app, "fooshop", "abcd", WithLogger(logger)) 86 | 87 | client.logBody(nil, "") 88 | if out.String() != "" { 89 | t.Errorf("logBody expected empty log output but received \"%s\"", out.String()) 90 | } 91 | 92 | client.logRequest(nil) 93 | if out.String() != "" { 94 | t.Errorf("logRequest expected empty log output received \"%s\"", out.String()) 95 | } 96 | 97 | client.logRequest(&http.Request{ 98 | Method: "GET", 99 | URL: &url.URL{Host: "http://test.com", Path: "/foo/1"}, 100 | Body: ioutil.NopCloser(strings.NewReader("request body")), 101 | }) 102 | 103 | if out.String() != reqExpected { 104 | t.Errorf("doGetHeadersDebug expected stdout \"%s\" received \"%s\"", reqExpected, out) 105 | } 106 | 107 | err.Reset() 108 | out.Reset() 109 | 110 | client.logResponse(nil) 111 | if out.String() != "" { 112 | t.Errorf("logResponse expected empty log output received \"%s\"", out.String()) 113 | } 114 | 115 | client.logResponse(&http.Response{ 116 | Status: http.StatusText(http.StatusOK), 117 | StatusCode: http.StatusOK, 118 | Header: map[string][]string{"X-Request-Id": {"00000000-0000-0000-0000-000000000000"}}, 119 | Body: ioutil.NopCloser(strings.NewReader("response body")), 120 | }) 121 | 122 | if out.String() != resExpected { 123 | t.Errorf("doGetHeadersDebug expected stdout \"%s\" received \"%s\"", resExpected, out.String()) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | if "%1%"=="" ( 3 | set cmd=container 4 | ) else ( 5 | set cmd=%1% 6 | ) 7 | if "%cmd%"=="container" ( 8 | docker-compose build test 9 | goto :eof 10 | ) 11 | if "%cmd%"=="test" ( 12 | docker-compose run --rm test 13 | goto :eof 14 | ) 15 | if "%cmd%"=="coverage" ( 16 | docker-compose run --rm test sh -c "go test -coverprofile=coverage.out ./... && go tool cover -html coverage.out -o coverage.html" 17 | coverage.html 18 | goto :eof 19 | ) 20 | if "%cmd%"=="clean" ( 21 | docker image rm go-shopify 22 | del coverage.html 23 | del coverage.out 24 | goto :eof 25 | ) 26 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // Option is used to configure client with options 9 | type Option func(c *Client) 10 | 11 | // WithVersion optionally sets the api-version if the passed string is valid 12 | func WithVersion(apiVersion string) Option { 13 | return func(c *Client) { 14 | pathPrefix := defaultApiPathPrefix 15 | if len(apiVersion) > 0 && (apiVersionRegex.MatchString(apiVersion) || apiVersion == UnstableApiVersion) { 16 | pathPrefix = fmt.Sprintf("admin/api/%s", apiVersion) 17 | } 18 | c.apiVersion = apiVersion 19 | c.pathPrefix = pathPrefix 20 | } 21 | } 22 | 23 | // WithRetry sets the number of times a request will be retried if a rate limit or service unavailable error is returned. 24 | // Rate limiting can be either REST API limits or GraphQL Cost limits 25 | func WithRetry(retries int) Option { 26 | return func(c *Client) { 27 | c.retries = retries 28 | } 29 | } 30 | 31 | func WithLogger(logger LeveledLoggerInterface) Option { 32 | return func(c *Client) { 33 | c.log = logger 34 | } 35 | } 36 | 37 | // WithHTTPClient is used to set a custom http client 38 | func WithHTTPClient(client *http.Client) Option { 39 | return func(c *Client) { 40 | c.Client = client 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestWithVersion(t *testing.T) { 11 | c := MustNewClient(app, "fooshop", "abcd", WithVersion(testApiVersion)) 12 | expected := fmt.Sprintf("admin/api/%s", testApiVersion) 13 | if c.pathPrefix != expected { 14 | t.Errorf("WithVersion client.pathPrefix = %s, expected %s", c.pathPrefix, expected) 15 | } 16 | } 17 | 18 | func TestWithVersionNoVersion(t *testing.T) { 19 | c := MustNewClient(app, "fooshop", "abcd", WithVersion("")) 20 | expected := "admin" 21 | if c.pathPrefix != expected { 22 | t.Errorf("WithVersion client.pathPrefix = %s, expected %s", c.pathPrefix, expected) 23 | } 24 | } 25 | 26 | func TestWithoutVersionInInitiation(t *testing.T) { 27 | c := MustNewClient(app, "fooshop", "abcd") 28 | expected := "admin" 29 | if c.pathPrefix != expected { 30 | t.Errorf("WithVersion client.pathPrefix = %s, expected %s", c.pathPrefix, expected) 31 | } 32 | } 33 | 34 | func TestWithVersionInvalidVersion(t *testing.T) { 35 | c := MustNewClient(app, "fooshop", "abcd", WithVersion("9999-99b")) 36 | expected := "admin" 37 | if c.pathPrefix != expected { 38 | t.Errorf("WithVersion client.pathPrefix = %s, expected %s", c.pathPrefix, expected) 39 | } 40 | } 41 | 42 | func TestWithRetry(t *testing.T) { 43 | c := MustNewClient(app, "fooshop", "abcd", WithRetry(5)) 44 | expected := 5 45 | if c.retries != expected { 46 | t.Errorf("WithRetry client.retries = %d, expected %d", c.retries, expected) 47 | } 48 | } 49 | 50 | func TestWithLogger(t *testing.T) { 51 | logger := &LeveledLogger{Level: LevelDebug} 52 | c := MustNewClient(app, "fooshop", "abcd", WithLogger(logger)) 53 | 54 | if c.log != logger { 55 | t.Errorf("WithLogger expected logs to match %v != %v", c.log, logger) 56 | } 57 | } 58 | 59 | func TestWithUnstableVersion(t *testing.T) { 60 | c := MustNewClient(app, "fooshop", "abcd", WithVersion(UnstableApiVersion)) 61 | expected := fmt.Sprintf("admin/api/%s", UnstableApiVersion) 62 | if c.pathPrefix != expected { 63 | t.Errorf("WithVersion client.pathPrefix = %s, expected %s", c.pathPrefix, expected) 64 | } 65 | } 66 | 67 | func TestWithHTTPClient(t *testing.T) { 68 | c := MustNewClient(app, "fooshop", "abcd", WithHTTPClient(&http.Client{Timeout: 30 * time.Second})) 69 | expected := 30 * time.Second 70 | 71 | if c.Client.Timeout.String() != expected.String() { 72 | t.Errorf("WithVersion client.Client = %s, expected %s", c.Client.Timeout, expected) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /payouts.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | const payoutsBasePath = "shopify_payments/payouts" 11 | 12 | // PayoutsService is an interface for interfacing with the payouts endpoints of 13 | // the Shopify API. 14 | // See: https://shopify.dev/docs/api/admin-rest/2023-01/resources/payouts 15 | type PayoutsService interface { 16 | List(context.Context, interface{}) ([]Payout, error) 17 | ListAll(context.Context, interface{}) ([]Payout, error) 18 | ListWithPagination(context.Context, interface{}) ([]Payout, *Pagination, error) 19 | Get(context.Context, uint64, interface{}) (*Payout, error) 20 | } 21 | 22 | // PayoutsServiceOp handles communication with the payout related methods of the 23 | // Shopify API. 24 | type PayoutsServiceOp struct { 25 | client *Client 26 | } 27 | 28 | // A struct for all available payout list options 29 | type PayoutsListOptions struct { 30 | PageInfo string `url:"page_info,omitempty"` 31 | Limit int `url:"limit,omitempty"` 32 | Fields string `url:"fields,omitempty"` 33 | LastId uint64 `url:"last_id,omitempty"` 34 | SinceId uint64 `url:"since_id,omitempty"` 35 | Status PayoutStatus `url:"status,omitempty"` 36 | DateMin *OnlyDate `url:"date_min,omitempty"` 37 | DateMax *OnlyDate `url:"date_max,omitempty"` 38 | Date *OnlyDate `url:"date,omitempty"` 39 | } 40 | 41 | // Payout represents a Shopify payout 42 | type Payout struct { 43 | Id uint64 `json:"id,omitempty"` 44 | Date OnlyDate `json:"date,omitempty"` 45 | Currency string `json:"currency,omitempty"` 46 | Amount decimal.Decimal `json:"amount,omitempty"` 47 | Status PayoutStatus `json:"status,omitempty"` 48 | } 49 | 50 | type PayoutStatus string 51 | 52 | const ( 53 | PayoutStatusScheduled PayoutStatus = "scheduled" 54 | PayoutStatusInTransit PayoutStatus = "in_transit" 55 | PayoutStatusPaid PayoutStatus = "paid" 56 | PayoutStatusFailed PayoutStatus = "failed" 57 | PayoutStatusCancelled PayoutStatus = "canceled" 58 | ) 59 | 60 | // Represents the result from the payouts/X.json endpoint 61 | type PayoutResource struct { 62 | Payout *Payout `json:"payout"` 63 | } 64 | 65 | // Represents the result from the payouts.json endpoint 66 | type PayoutsResource struct { 67 | Payouts []Payout `json:"payouts"` 68 | } 69 | 70 | // List payouts 71 | func (s *PayoutsServiceOp) List(ctx context.Context, options interface{}) ([]Payout, error) { 72 | payouts, _, err := s.ListWithPagination(ctx, options) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return payouts, nil 77 | } 78 | 79 | // ListAll Lists all payouts, iterating over pages 80 | func (s *PayoutsServiceOp) ListAll(ctx context.Context, options interface{}) ([]Payout, error) { 81 | collector := []Payout{} 82 | 83 | for { 84 | entities, pagination, err := s.ListWithPagination(ctx, options) 85 | 86 | if err != nil { 87 | return collector, err 88 | } 89 | 90 | collector = append(collector, entities...) 91 | 92 | if pagination.NextPageOptions == nil { 93 | break 94 | } 95 | 96 | options = pagination.NextPageOptions 97 | } 98 | 99 | return collector, nil 100 | } 101 | 102 | func (s *PayoutsServiceOp) ListWithPagination(ctx context.Context, options interface{}) ([]Payout, *Pagination, error) { 103 | path := fmt.Sprintf("%s.json", payoutsBasePath) 104 | resource := new(PayoutsResource) 105 | 106 | pagination, err := s.client.ListWithPagination(ctx, path, resource, options) 107 | if err != nil { 108 | return nil, nil, err 109 | } 110 | 111 | return resource.Payouts, pagination, nil 112 | } 113 | 114 | // Get individual payout 115 | func (s *PayoutsServiceOp) Get(ctx context.Context, id uint64, options interface{}) (*Payout, error) { 116 | path := fmt.Sprintf("%s/%d.json", payoutsBasePath, id) 117 | resource := new(PayoutResource) 118 | err := s.client.Get(ctx, path, resource, options) 119 | return resource.Payout, err 120 | } 121 | -------------------------------------------------------------------------------- /redirect.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | const redirectsBasePath = "redirects" 9 | 10 | // RedirectService is an interface for interacting with the redirects 11 | // endpoints of the Shopify API. 12 | // See https://shopify.dev/docs/api/admin-rest/latest/resources/redirect 13 | type RedirectService interface { 14 | List(context.Context, interface{}) ([]Redirect, error) 15 | Count(context.Context, interface{}) (int, error) 16 | Get(context.Context, uint64, interface{}) (*Redirect, error) 17 | Create(context.Context, Redirect) (*Redirect, error) 18 | Update(context.Context, Redirect) (*Redirect, error) 19 | Delete(context.Context, uint64) error 20 | } 21 | 22 | // RedirectServiceOp handles communication with the redirect related methods of the 23 | // Shopify API. 24 | type RedirectServiceOp struct { 25 | client *Client 26 | } 27 | 28 | // Redirect represents a Shopify redirect. 29 | type Redirect struct { 30 | Id uint64 `json:"id"` 31 | Path string `json:"path"` 32 | Target string `json:"target"` 33 | } 34 | 35 | // RedirectResource represents the result from the redirects/X.json endpoint 36 | type RedirectResource struct { 37 | Redirect *Redirect `json:"redirect"` 38 | } 39 | 40 | // RedirectsResource represents the result from the redirects.json endpoint 41 | type RedirectsResource struct { 42 | Redirects []Redirect `json:"redirects"` 43 | } 44 | 45 | // List redirects 46 | func (s *RedirectServiceOp) List(ctx context.Context, options interface{}) ([]Redirect, error) { 47 | path := fmt.Sprintf("%s.json", redirectsBasePath) 48 | resource := new(RedirectsResource) 49 | err := s.client.Get(ctx, path, resource, options) 50 | return resource.Redirects, err 51 | } 52 | 53 | // Count redirects 54 | func (s *RedirectServiceOp) Count(ctx context.Context, options interface{}) (int, error) { 55 | path := fmt.Sprintf("%s/count.json", redirectsBasePath) 56 | return s.client.Count(ctx, path, options) 57 | } 58 | 59 | // Get individual redirect 60 | func (s *RedirectServiceOp) Get(ctx context.Context, redirectId uint64, options interface{}) (*Redirect, error) { 61 | path := fmt.Sprintf("%s/%d.json", redirectsBasePath, redirectId) 62 | resource := new(RedirectResource) 63 | err := s.client.Get(ctx, path, resource, options) 64 | return resource.Redirect, err 65 | } 66 | 67 | // Create a new redirect 68 | func (s *RedirectServiceOp) Create(ctx context.Context, redirect Redirect) (*Redirect, error) { 69 | path := fmt.Sprintf("%s.json", redirectsBasePath) 70 | wrappedData := RedirectResource{Redirect: &redirect} 71 | resource := new(RedirectResource) 72 | err := s.client.Post(ctx, path, wrappedData, resource) 73 | return resource.Redirect, err 74 | } 75 | 76 | // Update an existing redirect 77 | func (s *RedirectServiceOp) Update(ctx context.Context, redirect Redirect) (*Redirect, error) { 78 | path := fmt.Sprintf("%s/%d.json", redirectsBasePath, redirect.Id) 79 | wrappedData := RedirectResource{Redirect: &redirect} 80 | resource := new(RedirectResource) 81 | err := s.client.Put(ctx, path, wrappedData, resource) 82 | return resource.Redirect, err 83 | } 84 | 85 | // Delete an existing redirect. 86 | func (s *RedirectServiceOp) Delete(ctx context.Context, redirectId uint64) error { 87 | return s.client.Delete(ctx, fmt.Sprintf("%s/%d.json", redirectsBasePath, redirectId)) 88 | } 89 | -------------------------------------------------------------------------------- /scripttag.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const scriptTagsBasePath = "script_tags" 10 | 11 | // ScriptTagService is an interface for interfacing with the ScriptTag endpoints 12 | // of the Shopify API. 13 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/scripttag 14 | type ScriptTagService interface { 15 | List(context.Context, interface{}) ([]ScriptTag, error) 16 | Count(context.Context, interface{}) (int, error) 17 | Get(context.Context, uint64, interface{}) (*ScriptTag, error) 18 | Create(context.Context, ScriptTag) (*ScriptTag, error) 19 | Update(context.Context, ScriptTag) (*ScriptTag, error) 20 | Delete(context.Context, uint64) error 21 | } 22 | 23 | // ScriptTagServiceOp handles communication with the shop related methods of the 24 | // Shopify API. 25 | type ScriptTagServiceOp struct { 26 | client *Client 27 | } 28 | 29 | // ScriptTag represents a Shopify ScriptTag. 30 | type ScriptTag struct { 31 | CreatedAt *time.Time `json:"created_at"` 32 | Event string `json:"event"` 33 | Id uint64 `json:"id"` 34 | Src string `json:"src"` 35 | DisplayScope string `json:"display_scope"` 36 | UpdatedAt *time.Time `json:"updated_at"` 37 | } 38 | 39 | // The options provided by Shopify. 40 | type ScriptTagOption struct { 41 | Limit int `url:"limit,omitempty"` 42 | Page int `url:"page,omitempty"` 43 | SinceId uint64 `url:"since_id,omitempty"` 44 | CreatedAtMin time.Time `url:"created_at_min,omitempty"` 45 | CreatedAtMax time.Time `url:"created_at_max,omitempty"` 46 | UpdatedAtMin time.Time `url:"updated_at_min,omitempty"` 47 | UpdatedAtMax time.Time `url:"updated_at_max,omitempty"` 48 | Src string `url:"src,omitempty"` 49 | Fields string `url:"fields,omitempty"` 50 | } 51 | 52 | // ScriptTagsResource represents the result from the admin/script_tags.json 53 | // endpoint. 54 | type ScriptTagsResource struct { 55 | ScriptTags []ScriptTag `json:"script_tags"` 56 | } 57 | 58 | // ScriptTagResource represents the result from the 59 | // admin/script_tags/{#script_tag_id}.json endpoint. 60 | type ScriptTagResource struct { 61 | ScriptTag *ScriptTag `json:"script_tag"` 62 | } 63 | 64 | // List script tags 65 | func (s *ScriptTagServiceOp) List(ctx context.Context, options interface{}) ([]ScriptTag, error) { 66 | path := fmt.Sprintf("%s.json", scriptTagsBasePath) 67 | resource := &ScriptTagsResource{} 68 | err := s.client.Get(ctx, path, resource, options) 69 | return resource.ScriptTags, err 70 | } 71 | 72 | // Count script tags 73 | func (s *ScriptTagServiceOp) Count(ctx context.Context, options interface{}) (int, error) { 74 | path := fmt.Sprintf("%s/count.json", scriptTagsBasePath) 75 | return s.client.Count(ctx, path, options) 76 | } 77 | 78 | // Get individual script tag 79 | func (s *ScriptTagServiceOp) Get(ctx context.Context, tagId uint64, options interface{}) (*ScriptTag, error) { 80 | path := fmt.Sprintf("%s/%d.json", scriptTagsBasePath, tagId) 81 | resource := &ScriptTagResource{} 82 | err := s.client.Get(ctx, path, resource, options) 83 | return resource.ScriptTag, err 84 | } 85 | 86 | // Create a new script tag 87 | func (s *ScriptTagServiceOp) Create(ctx context.Context, tag ScriptTag) (*ScriptTag, error) { 88 | path := fmt.Sprintf("%s.json", scriptTagsBasePath) 89 | wrappedData := ScriptTagResource{ScriptTag: &tag} 90 | resource := &ScriptTagResource{} 91 | err := s.client.Post(ctx, path, wrappedData, resource) 92 | return resource.ScriptTag, err 93 | } 94 | 95 | // Update an existing script tag 96 | func (s *ScriptTagServiceOp) Update(ctx context.Context, tag ScriptTag) (*ScriptTag, error) { 97 | path := fmt.Sprintf("%s/%d.json", scriptTagsBasePath, tag.Id) 98 | wrappedData := ScriptTagResource{ScriptTag: &tag} 99 | resource := &ScriptTagResource{} 100 | err := s.client.Put(ctx, path, wrappedData, resource) 101 | return resource.ScriptTag, err 102 | } 103 | 104 | // Delete an existing script tag 105 | func (s *ScriptTagServiceOp) Delete(ctx context.Context, tagId uint64) error { 106 | return s.client.Delete(ctx, fmt.Sprintf("%s/%d.json", scriptTagsBasePath, tagId)) 107 | } 108 | -------------------------------------------------------------------------------- /scripttag_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/jarcoal/httpmock" 10 | ) 11 | 12 | func TestScriptTagList(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/script_tags.json", client.pathPrefix), 17 | httpmock.NewStringResponder(200, `{"script_tags": [{"id": 1},{"id": 2}]}`)) 18 | 19 | scriptTags, err := client.ScriptTag.List(context.Background(), nil) 20 | if err != nil { 21 | t.Errorf("ScriptTag.List returned error: %v", err) 22 | } 23 | 24 | expected := []ScriptTag{{Id: 1}, {Id: 2}} 25 | if !reflect.DeepEqual(scriptTags, expected) { 26 | t.Errorf("ScriptTag.List returned %+v, expected %+v", scriptTags, expected) 27 | } 28 | } 29 | 30 | func TestScriptTagCount(t *testing.T) { 31 | setup() 32 | defer teardown() 33 | 34 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/script_tags/count.json", client.pathPrefix), 35 | httpmock.NewStringResponder(200, `{"count": 3}`)) 36 | 37 | cnt, err := client.ScriptTag.Count(context.Background(), nil) 38 | if err != nil { 39 | t.Errorf("ScriptTag.Count returned error: %v", err) 40 | } 41 | 42 | expected := 3 43 | if cnt != expected { 44 | t.Errorf("ScriptTag.Count returned %d, expected %d", cnt, expected) 45 | } 46 | } 47 | 48 | func TestScriptTagGet(t *testing.T) { 49 | setup() 50 | defer teardown() 51 | 52 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/script_tags/1.json", client.pathPrefix), 53 | httpmock.NewStringResponder(200, `{"script_tag": {"id": 1}}`)) 54 | 55 | scriptTag, err := client.ScriptTag.Get(context.Background(), 1, nil) 56 | if err != nil { 57 | t.Errorf("ScriptTag.Get returned error: %v", err) 58 | } 59 | 60 | expected := &ScriptTag{Id: 1} 61 | if !reflect.DeepEqual(scriptTag, expected) { 62 | t.Errorf("ScriptTag.Get returned %+v, expected %+v", scriptTag, expected) 63 | } 64 | } 65 | 66 | func scriptTagTests(t *testing.T, tag ScriptTag) { 67 | expected := uint64(870402688) 68 | if tag.Id != expected { 69 | t.Errorf("tag.Id is %+v, expected %+v", tag.Id, expected) 70 | } 71 | } 72 | 73 | func TestScriptTagCreate(t *testing.T) { 74 | setup() 75 | defer teardown() 76 | 77 | httpmock.RegisterResponder("POST", fmt.Sprintf("https://fooshop.myshopify.com/%s/script_tags.json", client.pathPrefix), 78 | httpmock.NewBytesResponder(200, loadFixture("script_tags.json"))) 79 | 80 | tag0 := ScriptTag{ 81 | Src: "https://djavaskripped.org/fancy.js", 82 | Event: "onload", 83 | DisplayScope: "all", 84 | } 85 | 86 | returnedTag, err := client.ScriptTag.Create(context.Background(), tag0) 87 | if err != nil { 88 | t.Errorf("ScriptTag.Create returned error: %v", err) 89 | } 90 | scriptTagTests(t, *returnedTag) 91 | } 92 | 93 | func TestScriptTagUpdate(t *testing.T) { 94 | setup() 95 | defer teardown() 96 | 97 | httpmock.RegisterResponder("PUT", fmt.Sprintf("https://fooshop.myshopify.com/%s/script_tags/1.json", client.pathPrefix), 98 | httpmock.NewBytesResponder(200, loadFixture("script_tags.json"))) 99 | 100 | tag := ScriptTag{ 101 | Id: 1, 102 | Src: "https://djavaskripped.org/fancy.js", 103 | } 104 | 105 | returnedTag, err := client.ScriptTag.Update(context.Background(), tag) 106 | if err != nil { 107 | t.Errorf("ScriptTag.Update returned error: %v", err) 108 | } 109 | scriptTagTests(t, *returnedTag) 110 | } 111 | 112 | func TestScriptTagDelete(t *testing.T) { 113 | setup() 114 | defer teardown() 115 | 116 | httpmock.RegisterResponder("DELETE", fmt.Sprintf("https://fooshop.myshopify.com/%s/script_tags/1.json", client.pathPrefix), 117 | httpmock.NewStringResponder(200, "{}")) 118 | 119 | if err := client.ScriptTag.Delete(context.Background(), 1); err != nil { 120 | t.Errorf("ScriptTag.Delete returned error: %v", err) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /shipping_zone_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/jarcoal/httpmock" 9 | "github.com/shopspring/decimal" 10 | ) 11 | 12 | // shippingZoneTests tests if fields are properly parsed. 13 | func shippingZoneTests(t *testing.T, zone ShippingZone) { 14 | cases := []struct { 15 | field string 16 | expected interface{} 17 | actual interface{} 18 | }{ 19 | {"Id", uint64(1039932365), zone.Id}, 20 | {"Name", "Some zone", zone.Name}, 21 | {"WeightBasedShippingRates.0.Id", uint64(882078075), zone.WeightBasedShippingRates[0].Id}, 22 | {"WeightBasedShippingRates.0.ShippingZoneId", zone.Id, zone.WeightBasedShippingRates[0].ShippingZoneId}, 23 | {"WeightBasedShippingRates.0.Name", "Canada Air Shipping", zone.WeightBasedShippingRates[0].Name}, 24 | {"WeightBasedShippingRates.0.Price", decimal.NewFromFloat(25.00).String(), zone.WeightBasedShippingRates[0].Price.String()}, 25 | {"WeightBasedShippingRates.0.WeightLow", decimal.NewFromFloat(0).String(), zone.WeightBasedShippingRates[0].WeightLow.String()}, 26 | {"WeightBasedShippingRates.0.WeightHigh", decimal.NewFromFloat(11.0231).String(), zone.WeightBasedShippingRates[0].WeightHigh.String()}, 27 | {"PriceBasedShippingRates.0.Id", uint64(882078074), zone.PriceBasedShippingRates[0].Id}, 28 | {"PriceBasedShippingRates.0.Name", "$5 Shipping", zone.PriceBasedShippingRates[0].Name}, 29 | {"PriceBasedShippingRates.0.Price", decimal.NewFromFloat(5.05).String(), zone.PriceBasedShippingRates[0].Price.String()}, 30 | {"PriceBasedShippingRates.0.MinOrderSubtotal", decimal.NewFromFloat(40.0).String(), zone.PriceBasedShippingRates[0].MinOrderSubtotal.String()}, 31 | {"PriceBasedShippingRates.0.MaxOrderSubtotal", decimal.NewFromFloat(100.0).String(), zone.PriceBasedShippingRates[0].MaxOrderSubtotal.String()}, 32 | {"CarrierShippingRateProviders.0.Id", uint64(882078076), zone.CarrierShippingRateProviders[0].Id}, 33 | {"CarrierShippingRateProviders.0.ShippingZoneId", zone.Id, zone.CarrierShippingRateProviders[0].ShippingZoneId}, 34 | {"CarrierShippingRateProviders.0.CarrierServiceId", uint64(770241334), zone.CarrierShippingRateProviders[0].CarrierServiceId}, 35 | {"CarrierShippingRateProviders.0.FlatModifier", decimal.NewFromFloat(0).String(), zone.CarrierShippingRateProviders[0].FlatModifier.String()}, 36 | {"CarrierShippingRateProviders.0.PercentModifier", decimal.NewFromFloat(0).String(), zone.CarrierShippingRateProviders[0].PercentModifier.String()}, 37 | } 38 | 39 | for _, c := range cases { 40 | if c.expected != c.actual { 41 | t.Errorf("ShippingZone.%s returned %v, expected %v", c.field, c.actual, 42 | c.expected) 43 | } 44 | } 45 | } 46 | 47 | func TestShippingZoneListError(t *testing.T) { 48 | setup() 49 | defer teardown() 50 | 51 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/shipping_zones.json", client.pathPrefix), 52 | httpmock.NewStringResponder(500, "")) 53 | 54 | expectedErrMessage := "Unknown Error" 55 | 56 | shippingZones, err := client.ShippingZone.List(context.Background()) 57 | if shippingZones != nil { 58 | t.Errorf("ShippingZone.List returned shippingZones, expected nil: %v", err) 59 | } 60 | 61 | if err == nil || err.Error() != expectedErrMessage { 62 | t.Errorf("ShippingZone.List err returned %+v, expected %+v", err, expectedErrMessage) 63 | } 64 | } 65 | 66 | func TestShippingZoneList(t *testing.T) { 67 | setup() 68 | defer teardown() 69 | 70 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/shipping_zones.json", client.pathPrefix), 71 | httpmock.NewBytesResponder(200, loadFixture("shipping_zones.json"))) 72 | 73 | shippingZones, err := client.ShippingZone.List(context.Background()) 74 | if err != nil { 75 | t.Errorf("ShippingZone.List returned error: %v", err) 76 | } 77 | 78 | // Check that shippingZones were parsed 79 | if len(shippingZones) != 1 { 80 | t.Errorf("ShippingZone.List got %v shippingZones, expected: 1", len(shippingZones)) 81 | } 82 | 83 | zone := shippingZones[0] 84 | shippingZoneTests(t, zone) 85 | } 86 | -------------------------------------------------------------------------------- /storefrontaccesstoken.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const storefrontAccessTokensBasePath = "storefront_access_tokens" 10 | 11 | // StorefrontAccessTokenService is an interface for interfacing with the storefront access 12 | // token endpoints of the Shopify API. 13 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/storefrontaccesstoken 14 | type StorefrontAccessTokenService interface { 15 | List(context.Context, interface{}) ([]StorefrontAccessToken, error) 16 | Create(context.Context, StorefrontAccessToken) (*StorefrontAccessToken, error) 17 | Delete(context.Context, uint64) error 18 | } 19 | 20 | // StorefrontAccessTokenServiceOp handles communication with the storefront access token 21 | // related methods of the Shopify API. 22 | type StorefrontAccessTokenServiceOp struct { 23 | client *Client 24 | } 25 | 26 | // StorefrontAccessToken represents a Shopify storefront access token 27 | type StorefrontAccessToken struct { 28 | Id uint64 `json:"id,omitempty"` 29 | Title string `json:"title,omitempty"` 30 | AccessToken string `json:"access_token,omitempty"` 31 | AccessScope string `json:"access_scope,omitempty"` 32 | AdminGraphqlApiId string `json:"admin_graphql_api_id,omitempty"` 33 | CreatedAt *time.Time `json:"created_at,omitempty"` 34 | } 35 | 36 | // StorefrontAccessTokenResource represents the result from the admin/storefront_access_tokens.json endpoint 37 | type StorefrontAccessTokenResource struct { 38 | StorefrontAccessToken *StorefrontAccessToken `json:"storefront_access_token"` 39 | } 40 | 41 | // StorefrontAccessTokensResource is the root object for a storefront access tokens get request. 42 | type StorefrontAccessTokensResource struct { 43 | StorefrontAccessTokens []StorefrontAccessToken `json:"storefront_access_tokens"` 44 | } 45 | 46 | // List storefront access tokens 47 | func (s *StorefrontAccessTokenServiceOp) List(ctx context.Context, options interface{}) ([]StorefrontAccessToken, error) { 48 | path := fmt.Sprintf("%s.json", storefrontAccessTokensBasePath) 49 | resource := new(StorefrontAccessTokensResource) 50 | err := s.client.Get(ctx, path, resource, options) 51 | return resource.StorefrontAccessTokens, err 52 | } 53 | 54 | // Create a new storefront access token 55 | func (s *StorefrontAccessTokenServiceOp) Create(ctx context.Context, storefrontAccessToken StorefrontAccessToken) (*StorefrontAccessToken, error) { 56 | path := fmt.Sprintf("%s.json", storefrontAccessTokensBasePath) 57 | wrappedData := StorefrontAccessTokenResource{StorefrontAccessToken: &storefrontAccessToken} 58 | resource := new(StorefrontAccessTokenResource) 59 | err := s.client.Post(ctx, path, wrappedData, resource) 60 | return resource.StorefrontAccessToken, err 61 | } 62 | 63 | // Delete an existing storefront access token 64 | func (s *StorefrontAccessTokenServiceOp) Delete(ctx context.Context, Id uint64) error { 65 | return s.client.Delete(ctx, fmt.Sprintf("%s/%d.json", storefrontAccessTokensBasePath, Id)) 66 | } 67 | -------------------------------------------------------------------------------- /storefrontaccesstoken_test.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/jarcoal/httpmock" 10 | ) 11 | 12 | func storefrontAccessTokenTests(t *testing.T, StorefrontAccessToken StorefrontAccessToken) { 13 | expectedStr := "API Client Extension" 14 | if StorefrontAccessToken.Title != expectedStr { 15 | t.Errorf("StorefrontAccessToken.Title returned %+v, expected %+v", StorefrontAccessToken.Title, expectedStr) 16 | } 17 | 18 | expectedStr = "378d95641257a4ab3feff967ee234f4d" 19 | if StorefrontAccessToken.AccessToken != expectedStr { 20 | t.Errorf("StorefrontAccessToken.AccessToken returned %+v, expected %+v", StorefrontAccessToken.AccessToken, expectedStr) 21 | } 22 | 23 | expectedStr = "unauthenticated_read_product_listings" 24 | if StorefrontAccessToken.AccessScope != expectedStr { 25 | t.Errorf("StorefrontAccessToken.AccessScope returned %+v, expected %+v", StorefrontAccessToken.AccessScope, expectedStr) 26 | } 27 | 28 | expectedStr = "gid://shopify/StorefrontAccessToken/755357713" 29 | if StorefrontAccessToken.AdminGraphqlApiId != expectedStr { 30 | t.Errorf("StorefrontAccessToken.AdminGraphqlApiId returned %+v, expected %+v", StorefrontAccessToken.AdminGraphqlApiId, expectedStr) 31 | } 32 | 33 | d := time.Date(2016, time.June, 1, 14, 10, 44, 0, time.UTC) 34 | if !d.Equal(*StorefrontAccessToken.CreatedAt) { 35 | t.Errorf("StorefrontAccessToken.CreatedAt returned %+v, expected %+v", StorefrontAccessToken.CreatedAt, d) 36 | } 37 | } 38 | 39 | func TestStorefrontAccessTokenList(t *testing.T) { 40 | setup() 41 | defer teardown() 42 | 43 | httpmock.RegisterResponder("GET", fmt.Sprintf("https://fooshop.myshopify.com/%s/storefront_access_tokens.json", client.pathPrefix), 44 | httpmock.NewBytesResponder(200, loadFixture("storefront_access_tokens.json"))) 45 | 46 | storefrontAccessTokens, err := client.StorefrontAccessToken.List(context.Background(), nil) 47 | if err != nil { 48 | t.Errorf("StorefrontAccessToken.List returned error: %v", err) 49 | } 50 | 51 | if len(storefrontAccessTokens) != 1 { 52 | t.Errorf("StorefrontAccessToken.List got %v storefront access tokens, expected: 1", len(storefrontAccessTokens)) 53 | } 54 | 55 | storefrontAccessTokenTests(t, storefrontAccessTokens[0]) 56 | } 57 | 58 | func TestStorefrontAccessTokenCreate(t *testing.T) { 59 | setup() 60 | defer teardown() 61 | 62 | httpmock.RegisterResponder("POST", fmt.Sprintf("https://fooshop.myshopify.com/%s/storefront_access_tokens.json", client.pathPrefix), 63 | httpmock.NewBytesResponder(200, loadFixture("storefront_access_token.json"))) 64 | 65 | storefrontAccessToken := StorefrontAccessToken{ 66 | Title: "API Client Extension", 67 | } 68 | 69 | returnedStorefrontAccessToken, err := client.StorefrontAccessToken.Create(context.Background(), storefrontAccessToken) 70 | if err != nil { 71 | t.Errorf("StorefrontAccessToken.Create returned error: %v", err) 72 | } 73 | 74 | storefrontAccessTokenTests(t, *returnedStorefrontAccessToken) 75 | } 76 | 77 | func TestStorefrontAccessTokenDelete(t *testing.T) { 78 | setup() 79 | defer teardown() 80 | 81 | httpmock.RegisterResponder("DELETE", fmt.Sprintf("https://fooshop.myshopify.com/%s/storefront_access_tokens/755357713.json", client.pathPrefix), 82 | httpmock.NewStringResponder(200, "{}")) 83 | 84 | err := client.StorefrontAccessToken.Delete(context.Background(), 755357713) 85 | if err != nil { 86 | t.Errorf("StorefrontAccessToken.Delete returned error: %v", err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /theme.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const themesBasePath = "themes" 10 | 11 | // Options for theme list 12 | type ThemeListOptions struct { 13 | Role string `url:"role,omitempty"` 14 | Fields string `url:"fields,omitempty"` 15 | } 16 | 17 | // ThemeService is an interface for interfacing with the themes endpoints 18 | // of the Shopify API. 19 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/theme 20 | type ThemeService interface { 21 | List(context.Context, interface{}) ([]Theme, error) 22 | Create(context.Context, Theme) (*Theme, error) 23 | Get(context.Context, uint64, interface{}) (*Theme, error) 24 | Update(context.Context, Theme) (*Theme, error) 25 | Delete(context.Context, uint64) error 26 | } 27 | 28 | // ThemeServiceOp handles communication with the theme related methods of 29 | // the Shopify API. 30 | type ThemeServiceOp struct { 31 | client *Client 32 | } 33 | 34 | // Theme represents a Shopify theme 35 | type Theme struct { 36 | Id uint64 `json:"id"` 37 | Name string `json:"name"` 38 | Previewable bool `json:"previewable"` 39 | Processing bool `json:"processing"` 40 | Role string `json:"role"` 41 | ThemeStoreId uint64 `json:"theme_store_id"` 42 | AdminGraphqlApiId string `json:"admin_graphql_api_id"` 43 | CreatedAt *time.Time `json:"created_at"` 44 | UpdatedAt *time.Time `json:"updated_at"` 45 | } 46 | 47 | // ThemesResource is the result from the themes/X.json endpoint 48 | type ThemeResource struct { 49 | Theme *Theme `json:"theme"` 50 | } 51 | 52 | // ThemesResource is the result from the themes.json endpoint 53 | type ThemesResource struct { 54 | Themes []Theme `json:"themes"` 55 | } 56 | 57 | // List all themes 58 | func (s *ThemeServiceOp) List(ctx context.Context, options interface{}) ([]Theme, error) { 59 | path := fmt.Sprintf("%s.json", themesBasePath) 60 | resource := new(ThemesResource) 61 | err := s.client.Get(ctx, path, resource, options) 62 | return resource.Themes, err 63 | } 64 | 65 | // Update a theme 66 | func (s *ThemeServiceOp) Create(ctx context.Context, theme Theme) (*Theme, error) { 67 | path := fmt.Sprintf("%s.json", themesBasePath) 68 | wrappedData := ThemeResource{Theme: &theme} 69 | resource := new(ThemeResource) 70 | err := s.client.Post(ctx, path, wrappedData, resource) 71 | return resource.Theme, err 72 | } 73 | 74 | // Get a theme 75 | func (s *ThemeServiceOp) Get(ctx context.Context, themeId uint64, options interface{}) (*Theme, error) { 76 | path := fmt.Sprintf("%s/%d.json", themesBasePath, themeId) 77 | resource := new(ThemeResource) 78 | err := s.client.Get(ctx, path, resource, options) 79 | return resource.Theme, err 80 | } 81 | 82 | // Update a theme 83 | func (s *ThemeServiceOp) Update(ctx context.Context, theme Theme) (*Theme, error) { 84 | path := fmt.Sprintf("%s/%d.json", themesBasePath, theme.Id) 85 | wrappedData := ThemeResource{Theme: &theme} 86 | resource := new(ThemeResource) 87 | err := s.client.Put(ctx, path, wrappedData, resource) 88 | return resource.Theme, err 89 | } 90 | 91 | // Delete a theme 92 | func (s *ThemeServiceOp) Delete(ctx context.Context, themeId uint64) error { 93 | path := fmt.Sprintf("%s/%d.json", themesBasePath, themeId) 94 | return s.client.Delete(ctx, path) 95 | } 96 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // TransactionService is an interface for interfacing with the transactions endpoints of 9 | // the Shopify API. 10 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/transaction 11 | type TransactionService interface { 12 | List(context.Context, uint64, interface{}) ([]Transaction, error) 13 | Count(context.Context, uint64, interface{}) (int, error) 14 | Get(context.Context, uint64, uint64, interface{}) (*Transaction, error) 15 | Create(context.Context, uint64, Transaction) (*Transaction, error) 16 | } 17 | 18 | // TransactionServiceOp handles communication with the transaction related methods of the 19 | // Shopify API. 20 | type TransactionServiceOp struct { 21 | client *Client 22 | } 23 | 24 | // TransactionResource represents the result from the orders/X/transactions/Y.json endpoint 25 | type TransactionResource struct { 26 | Transaction *Transaction `json:"transaction"` 27 | } 28 | 29 | // TransactionsResource represents the result from the orders/X/transactions.json endpoint 30 | type TransactionsResource struct { 31 | Transactions []Transaction `json:"transactions"` 32 | } 33 | 34 | // List transactions 35 | func (s *TransactionServiceOp) List(ctx context.Context, orderId uint64, options interface{}) ([]Transaction, error) { 36 | path := fmt.Sprintf("%s/%d/transactions.json", ordersBasePath, orderId) 37 | resource := new(TransactionsResource) 38 | err := s.client.Get(ctx, path, resource, options) 39 | return resource.Transactions, err 40 | } 41 | 42 | // Count transactions 43 | func (s *TransactionServiceOp) Count(ctx context.Context, orderId uint64, options interface{}) (int, error) { 44 | path := fmt.Sprintf("%s/%d/transactions/count.json", ordersBasePath, orderId) 45 | return s.client.Count(ctx, path, options) 46 | } 47 | 48 | // Get individual transaction 49 | func (s *TransactionServiceOp) Get(ctx context.Context, orderId uint64, transactionId uint64, options interface{}) (*Transaction, error) { 50 | path := fmt.Sprintf("%s/%d/transactions/%d.json", ordersBasePath, orderId, transactionId) 51 | resource := new(TransactionResource) 52 | err := s.client.Get(ctx, path, resource, options) 53 | return resource.Transaction, err 54 | } 55 | 56 | // Create a new transaction 57 | func (s *TransactionServiceOp) Create(ctx context.Context, orderId uint64, transaction Transaction) (*Transaction, error) { 58 | path := fmt.Sprintf("%s/%d/transactions.json", ordersBasePath, orderId) 59 | wrappedData := TransactionResource{Transaction: &transaction} 60 | resource := new(TransactionResource) 61 | err := s.client.Post(ctx, path, wrappedData, resource) 62 | return resource.Transaction, err 63 | } 64 | -------------------------------------------------------------------------------- /usagecharge.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | const usageChargesPath = "usage_charges" 12 | 13 | // UsageChargeService is an interface for interacting with the 14 | // UsageCharge endpoints of the Shopify API. 15 | // See https://shopify.dev/docs/api/admin-rest/latest/resources/usagecharge#endpoints 16 | type UsageChargeService interface { 17 | Create(context.Context, uint64, UsageCharge) (*UsageCharge, error) 18 | Get(context.Context, uint64, uint64, interface{}) (*UsageCharge, error) 19 | List(context.Context, uint64, interface{}) ([]UsageCharge, error) 20 | } 21 | 22 | // UsageChargeServiceOp handles communication with the 23 | // UsageCharge related methods of the Shopify API. 24 | type UsageChargeServiceOp struct { 25 | client *Client 26 | } 27 | 28 | // UsageCharge represents a Shopify UsageCharge. 29 | type UsageCharge struct { 30 | BalanceRemaining *decimal.Decimal `json:"balance_remaining,omitempty"` 31 | BalanceUsed *decimal.Decimal `json:"balance_used,omitempty"` 32 | CreatedAt *time.Time `json:"created_at,omitempty"` 33 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 34 | Description string `json:"description,omitempty"` 35 | Currency string `json:"currency,omitempty"` 36 | Id uint64 `json:"id,omitempty"` 37 | Price *decimal.Decimal `json:"price,omitempty"` 38 | RiskLevel *decimal.Decimal `json:"risk_level,omitempty"` 39 | } 40 | 41 | // UsageChargeResource represents the result from the 42 | // /admin/recurring_application_charges/X/usage_charges/X.json endpoints 43 | type UsageChargeResource struct { 44 | Charge *UsageCharge `json:"usage_charge"` 45 | } 46 | 47 | // UsageChargesResource represents the result from the 48 | // admin/recurring_application_charges/X/usage_charges.json endpoint. 49 | type UsageChargesResource struct { 50 | Charges []UsageCharge `json:"usage_charges"` 51 | } 52 | 53 | // Create creates new usage charge given a recurring charge. *required fields: price and description 54 | func (r *UsageChargeServiceOp) Create(ctx context.Context, chargeId uint64, usageCharge UsageCharge) ( 55 | *UsageCharge, error, 56 | ) { 57 | path := fmt.Sprintf("%s/%d/%s.json", recurringApplicationChargesBasePath, chargeId, usageChargesPath) 58 | wrappedData := UsageChargeResource{Charge: &usageCharge} 59 | resource := &UsageChargeResource{} 60 | err := r.client.Post(ctx, path, wrappedData, resource) 61 | return resource.Charge, err 62 | } 63 | 64 | // Get gets individual usage charge. 65 | func (r *UsageChargeServiceOp) Get(ctx context.Context, chargeId uint64, usageChargeId uint64, options interface{}) ( 66 | *UsageCharge, error, 67 | ) { 68 | path := fmt.Sprintf("%s/%d/%s/%d.json", recurringApplicationChargesBasePath, chargeId, usageChargesPath, usageChargeId) 69 | resource := &UsageChargeResource{} 70 | err := r.client.Get(ctx, path, resource, options) 71 | return resource.Charge, err 72 | } 73 | 74 | // List gets all usage charges associated with the recurring charge. 75 | func (r *UsageChargeServiceOp) List(ctx context.Context, chargeId uint64, options interface{}) ( 76 | []UsageCharge, error, 77 | ) { 78 | path := fmt.Sprintf("%s/%d/%s.json", recurringApplicationChargesBasePath, chargeId, usageChargesPath) 79 | resource := &UsageChargesResource{} 80 | err := r.client.Get(ctx, path, resource, options) 81 | return resource.Charges, err 82 | } 83 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Return the full shop name, including .myshopify.com 11 | func ShopFullName(name string) string { 12 | name = strings.TrimSpace(name) 13 | name = strings.Trim(name, ".") 14 | if strings.Contains(name, "myshopify.com") { 15 | return name 16 | } 17 | return name + ".myshopify.com" 18 | } 19 | 20 | // Return the short shop name, excluding .myshopify.com 21 | func ShopShortName(name string) string { 22 | // Convert to fullname and remove the myshopify part. Perhaps not the most 23 | // performant solution, but then we don't have to repeat all the trims here 24 | // :-) 25 | return strings.Replace(ShopFullName(name), ".myshopify.com", "", -1) 26 | } 27 | 28 | // Return the Shop's base url. 29 | func ShopBaseUrl(name string) string { 30 | name = ShopFullName(name) 31 | return fmt.Sprintf("https://%s", name) 32 | } 33 | 34 | // Return the prefix for a metafield path 35 | func MetafieldPathPrefix(resource string, resourceId uint64) string { 36 | prefix := "metafields" 37 | if resource != "" { 38 | prefix = fmt.Sprintf("%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 uint64) string { 45 | prefix := "fulfillments" 46 | if resource != "" { 47 | prefix = fmt.Sprintf("%s/%d/fulfillments", resource, resourceId) 48 | } 49 | return prefix 50 | } 51 | 52 | type OnlyDate struct { 53 | time.Time 54 | } 55 | 56 | func (c *OnlyDate) UnmarshalJSON(b []byte) error { 57 | value := strings.Trim(string(b), `"`) 58 | if value == "" || value == "null" { 59 | *c = OnlyDate{time.Time{}} 60 | return nil 61 | } 62 | 63 | t, err := time.Parse("2006-01-02", value) 64 | if err != nil { 65 | return err 66 | } 67 | *c = OnlyDate{t} 68 | return nil 69 | } 70 | 71 | func (c *OnlyDate) MarshalJSON() ([]byte, error) { 72 | return []byte(c.String()), nil 73 | } 74 | 75 | // It seems shopify accepts both the date with double-quotes and without them, so we just stick to the double-quotes for now. 76 | func (c *OnlyDate) EncodeValues(key string, v *url.Values) error { 77 | v.Add(key, c.String()) 78 | return nil 79 | } 80 | 81 | func (c *OnlyDate) String() string { 82 | return `"` + c.Format("2006-01-02") + `"` 83 | } 84 | 85 | func TimePtr(v time.Time) *time.Time { 86 | return &v 87 | } 88 | -------------------------------------------------------------------------------- /webhook.go: -------------------------------------------------------------------------------- 1 | package goshopify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const webhooksBasePath = "webhooks" 10 | 11 | // WebhookService is an interface for interfacing with the webhook endpoints of 12 | // the Shopify API. 13 | // See: https://shopify.dev/docs/api/admin-rest/latest/resources/webhook 14 | type WebhookService interface { 15 | List(context.Context, interface{}) ([]Webhook, error) 16 | Count(context.Context, interface{}) (int, error) 17 | Get(context.Context, uint64, interface{}) (*Webhook, error) 18 | Create(context.Context, Webhook) (*Webhook, error) 19 | Update(context.Context, Webhook) (*Webhook, error) 20 | Delete(context.Context, uint64) error 21 | } 22 | 23 | // WebhookServiceOp handles communication with the webhook-related methods of 24 | // the Shopify API. 25 | type WebhookServiceOp struct { 26 | client *Client 27 | } 28 | 29 | // Webhook represents a Shopify webhook 30 | type Webhook struct { 31 | Id uint64 `json:"id"` 32 | Address string `json:"address"` 33 | Topic string `json:"topic"` 34 | Format string `json:"format"` 35 | CreatedAt *time.Time `json:"created_at,omitempty"` 36 | UpdatedAt *time.Time `json:"updated_at,omitempty"` 37 | Fields []string `json:"fields"` 38 | MetafieldNamespaces []string `json:"metafield_namespaces"` 39 | PrivateMetafieldNamespaces []string `json:"private_metafield_namespaces"` 40 | ApiVersion string `json:"api_version,omitempty"` 41 | } 42 | 43 | // WebhookOptions can be used for filtering webhooks on a List request. 44 | type WebhookOptions struct { 45 | Address string `url:"address,omitempty"` 46 | Topic string `url:"topic,omitempty"` 47 | } 48 | 49 | // WebhookResource represents the result from the admin/webhooks.json endpoint 50 | type WebhookResource struct { 51 | Webhook *Webhook `json:"webhook"` 52 | } 53 | 54 | // WebhooksResource is the root object for a webhook get request. 55 | type WebhooksResource struct { 56 | Webhooks []Webhook `json:"webhooks"` 57 | } 58 | 59 | // List webhooks 60 | func (s *WebhookServiceOp) List(ctx context.Context, options interface{}) ([]Webhook, error) { 61 | path := fmt.Sprintf("%s.json", webhooksBasePath) 62 | resource := new(WebhooksResource) 63 | err := s.client.Get(ctx, path, resource, options) 64 | return resource.Webhooks, err 65 | } 66 | 67 | // Count webhooks 68 | func (s *WebhookServiceOp) Count(ctx context.Context, options interface{}) (int, error) { 69 | path := fmt.Sprintf("%s/count.json", webhooksBasePath) 70 | return s.client.Count(ctx, path, options) 71 | } 72 | 73 | // Get individual webhook 74 | func (s *WebhookServiceOp) Get(ctx context.Context, webhookdId uint64, options interface{}) (*Webhook, error) { 75 | path := fmt.Sprintf("%s/%d.json", webhooksBasePath, webhookdId) 76 | resource := new(WebhookResource) 77 | err := s.client.Get(ctx, path, resource, options) 78 | return resource.Webhook, err 79 | } 80 | 81 | // Create a new webhook 82 | func (s *WebhookServiceOp) Create(ctx context.Context, webhook Webhook) (*Webhook, error) { 83 | path := fmt.Sprintf("%s.json", webhooksBasePath) 84 | wrappedData := WebhookResource{Webhook: &webhook} 85 | resource := new(WebhookResource) 86 | err := s.client.Post(ctx, path, wrappedData, resource) 87 | return resource.Webhook, err 88 | } 89 | 90 | // Update an existing webhook. 91 | func (s *WebhookServiceOp) Update(ctx context.Context, webhook Webhook) (*Webhook, error) { 92 | path := fmt.Sprintf("%s/%d.json", webhooksBasePath, webhook.Id) 93 | wrappedData := WebhookResource{Webhook: &webhook} 94 | resource := new(WebhookResource) 95 | err := s.client.Put(ctx, path, wrappedData, resource) 96 | return resource.Webhook, err 97 | } 98 | 99 | // Delete an existing webhooks 100 | func (s *WebhookServiceOp) Delete(ctx context.Context, Id uint64) error { 101 | return s.client.Delete(ctx, fmt.Sprintf("%s/%d.json", webhooksBasePath, Id)) 102 | } 103 | --------------------------------------------------------------------------------