GithubHelp home page GithubHelp logo

shopify's Introduction

Shopify API

Build Status Hex.pm

This package allows Elixir developers to easily access the admin Shopify API.

Installation

The package can be installed by adding shopify to your list of dependencies in mix.exs:

def deps do
  [{:shopify, "~> 0.4"}]
end

Getting Started

The Shopify API can be accessed in two ways - either with private apps via basic auth, or with oauth.

Private Apps

Once you have a valid API key and password, setup your config/config.exs.

config :shopify, [
  shop_name: "my-shop",
  api_key: System.get_env("SHOPIFY_API_KEY"),
  password: System.get_env("SHOPIFY_API_PASSWORD")
]

We can now easily create a new API session.

Shopify.session

Alternatively, we can create a one-off session.

Shopify.session("my-shop-name", "my-api-key", "my-password")

OAuth Apps

Once you have a shopify app client ID and secret, setup your config/config.exs.

config :shopify, [
  client_id: System.get_env("SHOPIFY_CLIENT_ID"),
  client_secret: System.get_env("SHOPIFY_CLIENT_SECRET")
]

To gain access to a shop via OAuth, first, generate a permission url based on your requirments.

params = %{scope: "read_orders,read_products", redirect_uri: "http://my-redirect_uri.com/"}
permission_url = "shop-name" |> Shopify.session() |> Shopify.OAuth.permission_url(params)

After a shop has authorized access, they will be redirected to your URI above. The redirect will include a payload that contains a 'code'. We can now generate an access token.

{:ok, %Shopify.Response{data: oauth}} = "shop-name" |> Shopify.session() |> Shopify.OAuth.request_token(code)

We can now easily create a new OAuth API session.

Shopify.session("shop-name", oauth.access_token)

Making Requests

All API requests require a session struct to begin.

"shop-name" |> Shopify.session("access-token") |> Shopify.Product.find(1)

# OR

session = Shopify.session("shop-name", "access-token")
Shopify.Product.find(session, 1)

Here are some examples of the various types of requests that can be made.

# Create a session struct
session = Shopify.session("shop-name", "access-token")

# Find a resource by ID
{:ok, %Shopify.Response{data: product}} = session |> Shopify.Product.find(id)

# Find a resource and select fields
{:ok, %Shopify.Response{data: product}} = session |> Shopify.Product.find(id, %{fields: "id,images,title"})

# All resources
{:ok, %Shopify.Response{data: products}} = session |> Shopify.Product.all

# All resources with query params
{:ok, %Shopify.Response{data: products}} = session |> Shopify.Product.all(%{page: 1, limit: 5})

# Find a resource and update it
{:ok, %Shopify.Response{data: product}} = session |> Shopify.Product.find(id)
updated_product = %{product | title: "New Title"}
{:ok, response} = session |> Shopify.Product.update(product.id, updated_product)

# Update a resource without finding it
{:ok, response} = session |> Shopify.Product.update(id, %{title: "New Title"})

# Create a resource from the resource struct
new_product = %Shopify.Product{
    title: "Fancy Shirt",
    body_html: "<strong>Good shirt!<\/strong>",
    vendor: "Fancy Vendor",
    product_type: "shirt",
    variants: [
    	%{
   		price: "10.00",
    		sku: 123
   	}]
    }
{:ok, response} = session |> Shopify.Product.create(new_product)

# Create a resource from a simple map
new_product_args = %{
    title: "Fancy Shirt",
    body_html: "<strong>Good shirt!<\/strong>",
    vendor: "Fancy Vendor",
    product_type: "shirt",
    variants: [
    	%{
   		price: "10.00",
    		sku: 123
   	}]
    }
{:ok, response} = session |> Shopify.Product.create(new_product_args)

# Count resources
{:ok, %Shopify.Response{data: count}} = session |> Shopify.Product.count

# Count resources with query params
{:ok, %Shopify.Response{data: count}} = session |> Shopify.Product.count(%{vendor: "Fancy Vendor"})

# Search for resources
{:ok, %Shopify.Response{data: customers}} = session |> Shopify.Customer.search(%{query: "country:United States"})

# Delete a resource
{:ok, _} = session |> Shopify.Product.delete(id)

API Versioning

Shopify supports API versioning. By default, if you dont specify an api version, your request defaults to the oldest supported stable version.

You can specify a default version through application config.

config :shopify, [
  api_version: "2019-04"
]

You can also set a specific version per session.

Shopify.session("shop-name", "access-token") |> Shopify.Session.put_api_version("2019-04")

Handling Responses

Responses are all returned in the form of a two-item tuple. Any response that has a status code below 300 returns {:ok, response}. Codes above 300 are returned as {:error, response}.

# Create a session struct
session = Shopify.session("shop-name", "access-token")

# 'data' is returned as a %Shopify.Product struct
{:ok, %Shopify.Response{code: 200, data: data}} = session |> Shopify.Product.find(id)

# 'data' is returned as a list of %Shopify.Product structs
{:ok, %Shopify.Response{code: 200, data: data}} = session |> Shopify.Product.all

# 'message' is a text description of the error.
{:error, %Shopify.Response{code: 404, data: message}} = session |> Shopify.Product.find(1)

# Failed requests return %Shopify.Error struct
{:error, %Shopify.Error{reason: :econnrefused, source: :httpoison}} = session |> Shopify.Product.find(1)

The %Shopify.Response{} struct contains two fields: code and data. Code is the HTTP status code that is returned from Shopify. A successful request will either set the data field with a single struct, or list of structs of the resource or resources requested.

Multipass

The Multipass is available to Shopify Plus plans. It allows your non-Shopify site to be the source of truth for authentication and login. After your site has successfully authenticated a user, redirect their browser to Shopify using the special Multipass URL: this will upsert the customer data in Shopify and log them in.

Unlike other API requests, this does not require a session: it relies on a shared secret to do decryption.

Your customer data must at a minimum provide an email address and a current datetime in 8601 format.

customer_data = %{
  email: "[email protected]",
  created_at: DateTime.to_iso8601(Timex.now())
}

# From your store's checkout settings
multipass_secret = Application.get_env("MULTIPASS_SECRET")

url = Shopify.Multipass.get_url("myteststore", customer_data, multipass_secret)

# Redirect the browser immediately to the resulting URL:
"https://myteststore.myshopify.com/account/login/multipass/moaqEVx1Yu9hsvYvVpj-LeRYDtOo6ikicfTZd8tR8-xBMRg8tFjGEfllEcjj2VdbsezmT0XuEdglyQzi_biQPkfLJnP1dkxhNtfzwtt6IMQzu3W0qCPzbrUMD_gLaytPVP-zZZuYiSBqEMNdvzFg3zf0TOQHwbizX2D7It02sFI7ZpTRhfX4m_crV0b-DmmF"

Testing

For testing a mock adapter can be configured to use fixture json files instead of doing real requests.

Lets say you have a test config file in your_project/config/test.exs and tests in your_project/test you could use this configuration:

# your_project/config/test.exs
config :shopify, [
  shop_name: "test",
  api_key: "test-key",
  password: "test-paswword",
  client_secret: "test-secret",
  client_adapter: Shopify.Adapters.Mock, # Use included Mock adapter
  fixtures_path: Path.expand("../test/fixtures/shopify", __DIR__) # Use fixures in this directory
]

When using oauth, make sure the token passed is test, otherwise authentication will fail.

Shopify.session("my-shop.myshopify.com", "test")
|> Product.all()

Test Adapter

This plugin provides a test adapter called Shopify.Adapters.Mock to use out of the box. It makes certain assumptions about your fixtures and is limited to the responses provided in corresponding fixture files, and for create actions it will put the resource id as 1.

If you would like to roll your own adapter, you can do so by implementing @behaviour Shopify.Adapters.Base.

defmodule Shopify.Adapters.Mock do
  @moduledoc false

  @behaviour Shopify.Adapters.Base

  def get(%Shopify.Request{} = request) do
    data =  %{resource: %{id: 123, attribute: "attribute"}}
    {:ok,  %Shopify.Response{code: 200, data: data}}
  end

  # ...
end

Fixtures

Fixture files must follow a certain structure, so the adapter is able to find them. If your resource is Shopify.Product.all() you need to provide a file at path_you_provided_in_config/products.json and must include a valid response json

{
  "orders": [
    {
      "buyer_accepts_marketing": false,
      "cancel_reason": null,
      "cancelled_at": null,
      ...
    }
  ]
}

Or for Shopify.Product.find(1)

# path_you_provided_in_config/products/1.json
{
  "order": {
    "id": 1,
    "email": "[email protected]",
    ...
  }
}

Current Resources

  • Address
  • ApplicationCharge (find, all, create, activate)
  • ApplicationCredit (find, all, create)
  • Article (find, all, create, update, delete, count)
  • Article.Author (all)
  • Article.Tag (all)
  • Attribute
  • BillingAddress
  • Blog (find, all, create, update, delete, count)
  • CarrierService (find, all, create, update, delete)
  • Checkout (all, find, create, update, count, shipping_rates, complete, count)
  • ClientDetails
  • Collect (find, all, create, delete, count)
  • CollectionListing (find, all)
  • Comment (find, all, create, update, spam, not_spam, approve, remove, restore)
  • Country (find, all, create, update, delete, count)
  • Country.Province (find, all, update, count)
  • CustomCollection (find, all, create, update, delete, count)
  • Customer (find, all, create, update, delete, count, search)
  • CustomerAddress (find, all, create, delete)
  • CustomerSavedSearch (find, all, create, update, delete, count)
  • CustomerSavedSearch.Customer (all)
  • DiscountCode
  • DraftOrder (find, all, create, update, delete, count, complete, send_invoice) send_invoice is an alias of DraftOrder.DraftOrderInvoice.create/3
  • DraftOrder.DraftOrderInvoice (create)
  • MarketingEvent.Engagement (create_multiple)
  • Event (find, all, count)
  • Order.Fullfillment (find, all, count, create, update, complete, open, cancel)
  • Order.Fullfillment.Event (find, all, delete)
  • FulfillmentService (find, all, create, update, delete)
  • Image (ProductImage) (find, all, create, update, delete, count)
  • InventoryLevel (all, delete)
  • LineItem
  • Location (find, all, count)
  • MarketingEvent (find, all, count, create, update, delete, create_multiple_engagements) create_multiple_engagements is an alias of MarketingEvent.Engagement.create_multiple/3
  • Metafield
  • OAuth.AccessScope (all)
  • Option
  • Order (find, all, create, update, delete, count)
  • Order.Event (all)
  • Order.Risk (create, find, all, update, delete)
  • Page (create, find, all, update, delete, count)
  • PaymentDetails
  • Policy (all)
  • PriceRule (find, all, create, update, delete)
  • PriceRule.DiscountCode (find, all, create, update, delete)
  • Product (find, all, create, update, delete, count)
  • Product.Event (all)
  • ProductListing (find, all, create, update, delete, count, product_ids)
  • RecurringApplicationCharge (find, all, create, activate, delete)
  • Redirect (find, all, create, update, delete, count)
  • Refund (create, find, all)
  • Report (create, find, all, update, delete)
  • ScriptTag (find, all, create, count, delete)
  • ShippingAddress
  • ShippingLine
  • Shop (current)
  • SmartCollection (find, all, create, count, update, delete)
  • TaxLine
  • Theme (find, all, create, update, delete)
  • Theme.Asset (find, all, delete)
  • Transaction (find, all, create, count)
  • UsageCharge (find, all, create)
  • Variant (find, all, create, update, delete, count)
  • Webhook (find, all, create, update, delete, count)

Contributors

| Nick Sweeting
Nick Sweeting
๐Ÿ’ป ๐Ÿ‘€ ๐Ÿ“– ๐Ÿš‡ | Marcelo Oliveira
Marcelo Oliveira
๐Ÿ’ป | Fabian Zitter
Fabian Zitter
๐Ÿ’ป ๐Ÿ‘€ ๐Ÿ“– | Zach Garwood
Zach Garwood
๐Ÿ’ป | David Becerra
David Becerra
๐Ÿ’ป | Bryan Bryce
Bryan Bryce
๐Ÿ“– | humancopy
humancopy
๐Ÿ’ป | Cmeurer10
Cmeurer10
๐Ÿ’ป | lewisf
lewisf
๐Ÿ’ป | vladimir-e
vladimir-e
๐Ÿ’ป | furqanaziz
furqanaziz
๐Ÿ’ป | balexand
balexand
๐Ÿ’ป | :---: | :---: | :---: | :---: | :---: | :---: | :---: |

This project follows the all-contributors specification.

Documentation is generated with ExDoc. They can be found at https://hexdocs.pm/shopify.

shopify's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

shopify's Issues

Use nested modules to reflect nested resources?

When I was adding some more resources I became kind of blind to the details, pretty much lost in copy pasting and going through the same changes over and over and whenever I did a nested resource I thought "how exactly would anyone use this and know where the first id is coming from?"

For example

session |> Fulfillment.all(123123)
session |> UsageCharge.find(987987123, 123)
session |> Transaction.count(879879798)

If you know that a fulfillment means "order fulfillment", usage charge is a kind of "recurring application charge" and transaction again refers to an order, then of course you know what those IDs are, but there is no hint whatsoever in the code. My proposal would be to use nested module names like this:

session |> Order.Fulfillment.all(123123)
session |> RecurringApplicationCharge.UsageCharge.find(987987123, 123)
session |> Order.Transaction.count(879879798)

alternatively the function names could be changed to reflect the "base resource"

session |> Fulfillment.all_for_order(123123)
session |> UsageCharge.find_for_recurring_application_charge(987987123, 123)
session |> Transaction.count_for_order(879879798)

I like the nested module better, because you can just alias them and use the shorter version if you like, whereas the function names would always be long.

We could do this without breaking older versions and simply add the alternative to the existing solutions, and add a deprecation warning to the old version.

Let me know what you think @nsweeting do you think we should do this at all, and if so which of the solutions would you like better?

ScripTags Not Working

I'm trying to perform a scripttag, but it shows me this error:

Shopify.ScriptTag.create(session, [event: "onload", src: "https://djavaskripped.org/fancy.js"])

The result:

** (FunctionClauseError) no function clause matching in Map.from_struct/1    
    
    The following arguments were given to Map.from_struct/1:
    
        # 1
        [event: "onload", src: "https://djavaskripped.org/fancy.js"]
    
    Attempted function clauses (showing 2 out of 2):
    
        def from_struct(struct) when is_atom(struct)
        def from_struct(%_{} = struct)
    
    (elixir) lib/map.ex:849: Map.from_struct/1
    (shopify) lib/shopify/resources/script_tag.ex:6: Shopify.ScriptTag.to_json/1
    (shopify) lib/shopify/resources/script_tag.ex:6: Shopify.ScriptTag.create/2

I don't know if I'm doing it right ?

Add missing resource tests

Tests are missing some available resources, it is basically a copy-paste-and-change-the-names job. The most tedious part is to make an example request to shopify to set up a request yaml.

:tls_alert, 'handshake failure' on Poison

Well, after workaround fixes #4 and #3 i start receiving this error of poison, after some search i found this: edgurgel/httpoison#164 (comment) , so i update Client.post function to:

HTTPoison.post(request.full_url, request.body, request.headers, hackney: [pool: :shopify], ssl: [{:versions, [:'tlsv1.2']}])```

and works fine =).

Note to author: Im sorry report all this bugs but i really like your shopify plugin, is the best in Elixir language, so i trying to help because im using happy =). Good work on this.


Error doing requests

After workaround #3 i had this error when i was trying retrieve access token again:

** (exit) exited in: :gen_server.call(:hackney_manager, {:new_request, #PID<0.1264.0>, #Reference<0.0.2.16709>, {:client, :undefined, {:metrics_ng, :metrics_dummy}, :hackney_ssl,

I do some research and problem is that httppoison is not being started, dont have sure why, so i added in my code HTTPoison.start before try get access token and worked fine.

Add tests to validate structs

To prevent issues like #45 we should add tests that read out the fixture keys and validate that the struct actually defines the correct fields.

Ideally we would not write a test for every single field in the struct, but use generated code to iterate over the keys and validate them automatically

Shop name string treatment

Well, this is an convenience and i think is really helpful, i lost some time trying figure out what was causing this:

Shopify.session(shop) |> Shopify.OAuth.request_token(params["code"]) returning {:error, 404..}

My shop string (returned from shopify callback) was my-store.myshopify.com and this was the problem, so i do this:

shop = String.replace(params["shop"], ".myshopify.com", "") and to my surprise works fine and im receiving my {:ok, ...access code and stuff} now. Everything works fine.

Set timeout for HTTPoison requests

Now when I fixed #38 in my fork, I notice that I get too much :timeouts from HTTPoison. The default : recv_timeout in HTTPoison is 5000ms. That's enough for most of requests, but when I create a product with multiple images, shopify just unable to download all images in such a short time (might be due to a slow third-party server that host images).

The lib already has a handy struct that we passing around - session. Do you think we could add httpoison_options that will override @options in HTTP adapter:

defmodule Shopify.Adapters.HTTP do
  @moduledoc false

  @behaviour Shopify.Adapters.Base
  @options [hackney: [pool: :shopify], ssl: [{:versions, [:"tlsv1.2"]}]]

  def get(request) do
    HTTPoison.get(request.full_url, request.headers, @options)
    |> handle_response(request.resource)
  end
  # ...

Example usage could be:

session_longer_timeout = session |> Map.put(:httpoison, %{recv_timeout: 15000})
Shopify.Product.create(session_longer_timeout, product_with_lots_of_images)

Error when activating charge

Hello, I am trying to activate a RecurringApplicationCharge. I am getting the following case clause error:

Shopify.RecurringApplicationCharge.activate(session, charge.id)
** (CaseClauseError) no case clause matching: nil
    (hackney) deps/hackney/src/hackney_request.erl:312: :hackney_request.handle_body/4
    (hackney) deps/hackney/src/hackney_request.erl:81: :hackney_request.perform/2
    (hackney) deps/hackney/src/hackney.erl:358: :hackney.send_request/2
    (httpoison) lib/httpoison/base.ex:439: HTTPoison.Base.request/9
    (shopify) lib/shopify/adapters/http.ex:13: Shopify.Adapters.HTTP.post/1

My session works fine with other requests:

%Shopify.Session{
  access_token: "mytoken",
  api_key: nil,
  client_id: nil,
  client_secret: nil,
  password: nil,
  shop_name: "myshop",
  type: :oauth
}

I can use the session and charge.id to call Shopify.RecurringApplicationCharge.find/2 which returns the correct response, but when I try and call activate it errors out.

Has anyone else run into this issue as well?

Unhandled CaseClauseError from bug in Posion

When Shopify.Response is trying to parse the body of the response, I'm seeing a CaseClauseError exception occur sometimes from this line. It's because of a bug in Poison (I'm using Poison version 3.1.0). It's not a documented signature, so I understand it may not be this shopify lib's responsibility to fix it, however we could prevent it by modifying the mix.exs requirement for poison to either avoid this bugged version, or add a clause to capture it.

Poison 4.x returns to a 2-item error tuple so it should be compatible again.

Handle timeout and other possible `HTTPoison` errors

Hello. First of all thanks a lot for making this lib!

I faced a problem while using the lib and would like to discuss a solution, so I can submit a PR fixing it.

During long-running process of importing products via Shopify API, we occasionally get the following error:

Shopify.Product.create(session, payload)

%FunctionClauseError{args: nil, arity: 2, clauses: nil, function: :handle_response, kind: nil, module: Shopify.Adapters.HTTP}

It happens here:

https://github.com/nsweeting/shopify/blob/master/lib/shopify/adapters/http.ex

defmodule Shopify.Adapters.HTTP do
  # ...
  def handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, resource) do
    Shopify.Response.new(code, body, resource)
  end

I believe we could add another clause for handle_response catching everything else and return something like:

  def handle_response(response, resource) do
    {:error, response, resource}
  end

Maybe it would be worthwhile creating Shopify.Error to "standardize" the error format.

Please let me know what you think? If I at least start on adding a clause for handle_response figuring out the details of implementation as I go, would you accept such PR?

Currently I use this workaround:

    try do
      result = Product.create(session, payload)

    rescue
      # Catch unhandled timeout error in Shopify lib
      e in FunctionClauseError ->
        {:error, record, e}
    end

Confused about shop-name?

How do you know what the shopname is before the Shop gives access to your app?
hardcoding shop name seems silly unless im understanding everything wrong.

Using fixtures when searching via query params

Is there support for using the fixture files when searching via query parameters? If so what is the file format name, or does it look inside the fixture data?

Ex: Shopify.session |> Shopify.Order.all(%{order_number: 1024, status: "any"})

[PriceRule] Missing field

Hi there,

I noticed the Shopify.PriceRule module is missing the: prerequisite_quantity_range

Regards,
Antonio

@adapter Shopify.Config.get(:client_adapter) returning nil

I was trying retrieve access token:

a = Shopify.session(params["shop"]) |> Shopify.OAuth.request_token(params["code"])

but i have the error nil.post undefined so i do search in the library code and see that @adapter Shopify.Config.get(:client_adapter) was returning nil, don`t have sure why, i do an workaround here with code above to continue using until bug fixed:

  @adapter Shopify.Config.get(:client_adapter) || Shopify.Adapters.HTTP

Rate-limiting

I'm curious to learn how you are handling rate-limiting. And do you have any plans to include rate-limiting functionality in the library itself?

Is this project dead?

Seems to be tons of open PRs without any responses from the @nsweeting.

I rely on this package for a number of apps. Are there any actively maintained forks of this?

Add Versioning Support

As per the recent announcement, we will need to add support for versioning.

The shopify session struct should most likely carry this information around. I'm envisioning a default version could be set in the application config. But we could also add a Shopify.Session.put_version/2 function that allows one to explicitly set a new version.

Add missing resources

There are still some resources missing according to this list.

Since most of them follow the same pattern, I think this is a good first issue.

EDIT: Here is a list of resources I think we are missing. This might be incomplete or accidentally list a resource we already have under a different name, so before diving in please make sure I did not make a mistake first ๐Ÿ™‡

  • AbandonedCheckout (just realized this is the same as Checkout
  • Fulfillment โ—
  • FulfillmentEvent
  • FulfillmentService
  • GiftCard (Shopify PLUS only!) ๐Ÿ’ค
  • InventoryItem
  • InventoryLevel
  • Location โ—
  • MarketingEvent
  • Multipass (SHOPIFY PLUS only!) ๐Ÿ’ค
  • OrderRisk
  • Page
  • Policy
  • ProductListing
  • Refund
  • Report

Problems With OAuth Flow using Explicit API Version

The following functions dealing with the OAuth flow inserted the API version into the request url, but it seems this is not desired (as mentioned in a Shopify Partners blog post).
Shopify.OAuth.request_token/2

iex> shopify_domain
...> |> Shopify.session()
...> |> Shopify.OAuth.request_token(shopify_token)
{:error, %Shopify.Response{code: 303, data: nil, headers: ...}}

Shopify.OAuth.permission_url/2

iex> shopify_domain
...> |> Shopify.session()
...> |> Shopify.OAuth.permission_url(params)
"....myshopify.com/admin/api/#{api_version}/oauth/authorize?..."

This permission url lead to a "page not found" error.

My current workaround is to nullify the API version for these two functions:

session =
  shop_name
  |> Shopify.session()
  |> Shopify.Session.put_api_version(nil)

All resource calls return 401 for private app

I'm hoping I just missed something obvious, but I can't get these two simple calls to return our data.

Shopify.session("my-shop-name", "my-api-key", "my-password")
|> Shopify.Product.all
{:error, %Shopify.Response{code: 401, data: nil}}

I've double-checked the API key and password are correct, and I've used both shop-name and shop-name.myshopify.com.

I am able to curl the example URL generated in the private app settings using the format https://apikey:password@hostname/admin/resource.json.

Any ideas?

Error when deleting a charge

Hello, I got an error when deleting a charge. It appears that the request to cancel works, but the parse_json/2 function in the Response module throws an error.

Shopify.RecurringApplicationCharge.delete(session, 1234)
> no case clause matching: {:error, :invalid, 0}
    (shopify) lib/shopify/response.ex:24: Shopify.Response.parse_json/2
    (shopify) lib/shopify/response.ex:12: Shopify.Response.new/3

It appears that parse_json is being called with the following arguments: parse_json(nil, "")

nil being the resource, and "" is the body.

This results in the following being called here:

Poison.decode("", as: nil)
> {:error, :invalid, 0}

You'll notice the result there is the same as my error above. I'm happy to take a stab at a PR for this, but I'm curious how we might want to handle the response here?

There's no response body from Shopify according to their docs. This seems inconsistent with other DELETE calls where the response body is an empty JSON object.

More detailed documentation

Thanks to all contributors for all your work on this project. It would be great to get some more detailed information regarding the installation, authentication process and recurring billing for public app usage with this package. Thanks again.

Shop info should be dynamically configurable

Apologies if this is actually possible and I'm just not seeing it, but, I think that all shop data should be dynamically configurable.

I'm thinking that it should be stored either in an external DB or Mnesia, somewhere where it could persist between deploys. The reason for this is that an app in the Shopify ecosystem will likely support multiple shops and credentials for each should be easily applied to queries against the API.

Order fulfillment event is using the wrong resource parser

hi there,

trying to get a order's fulfillment event using:
{:ok, response} = Shopify.session |> Shopify.Order.Fulfillment.Event.find(order_id, fulfillment_id, event_id)
but it's using the Shopify.Event as resource, which is wrong. Since the response should be something similar to this:

https://help.shopify.com/en/api/reference/shipping_and_fulfillment/fulfillmentevent#show

{
  "fulfillment_event": {
    "id": 944956392,
    "fulfillment_id": 255858046,
    "status": "in_transit",
    "message": null,
    "happened_at": "2018-07-05T12:58:55-04:00",
    "city": null,
    "province": null,
    "country": null,
    "zip": null,
    "address1": null,
    "latitude": null,
    "longitude": null,
    "shop_id": 690933842,
    "created_at": "2018-07-05T12:58:55-04:00",
    "updated_at": "2018-07-05T12:58:55-04:00",
    "estimated_delivery_at": null,
    "order_id": 450789469,
    "admin_graphql_api_id": "gid://shopify/FulfillmentEvent/944956392"
  }
}

and what I'm getting is:

{:ok, response} = Shopify.session |> Shopify.Order.Fulfillment.Event.find(order_id, fulfillment_id, event_id)

{:ok,
 %Shopify.Response{
   code: 200,
   data: [
     %Shopify.Event{
       arguments: nil,
       body: nil,
       created_at: "2018-09-17T19:39:27+01:00",
       description: nil,
       id: 111111111,
       message: nil,
       path: nil,
       subject_id: nil,
       subject_type: nil,
       verb: nil
     }
  ]
}}

[Multipass] Clear up API

For testability, the author of the original PR #65 made all functions public. In order to clear up which part is meant as the public API, we should:

either make the majority of the functions private, which would lose the ability to test them

or

add @doc false to non public-API functions

Remove douplicate code from Resource / NestedResource

Because we are defining singular_resource, plural_resource and to_json in both modules, linters will warn you about "functions can not match because a previous function will always match"

defmodule Shopify.Variant do
  @derive [Poison.Encoder]
  @singular "variant"
  @plural "variants"

  use Shopify.Resource, import: [
    :find
  ]

  # This will trigger warnings:
  use Shopify.NestedResource, import: [
    :create,
    :all,
    :count,
    :update,
    :delete
  ]
  # ...
end

One way to solve this would be to move to_json and the resource functions in respective modules.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.