Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create and Delete Profiles #3

Merged
merged 15 commits into from
Oct 22, 2019
Merged

Create and Delete Profiles #3

merged 15 commits into from
Oct 22, 2019

Conversation

cassidycodes
Copy link
Contributor

@cassidycodes cassidycodes commented Oct 18, 2019

This PR adds a class to interface with the /profiles endpoint. It implements both create and delete actions.

See my review below for more details.

)
# => {
# code: 1,
# message: 'Hup...want...buy.',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poor Hup got motion sickness: https://www.youtube.com/watch?v=Sqj8xY0_Q0s

lib/bambora/client.rb Outdated Show resolved Hide resolved
# frozen_string_literal: true

module Bambora
class JSONRequest
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is shared code for all JSON APIs. We'll also need an XMLRequest for some of the endpoints.


```ruby
client.profile.delete(1234)
# => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return from these methods is the same as what the existing beanstream-ruby library gives us.

end

def request(method:, path:, params: {}, body: {})
parse_response(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need any error handling here? If there is a network Error, we'll get an informative error from Excon.

Based on the existing gem, is possible that we'll get an error response back from Bambora, but those errors are in JSON as well, so I think we should handle them in the application.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess if server error happens(not application which will form correct json with error as the body) you won't get to JSON response instead you'll get JSON parse exception and this is our responsibility to handle it. Possible solution is to check for response code first and if it signals about error wrap it in valid JSON.

@cassidycodes cassidycodes self-assigned this Oct 18, 2019
@cassidycodes cassidycodes added enhancement New feature or request documentation Improvements or additions to documentation labels Oct 18, 2019
@cassidycodes cassidycodes changed the base branch from chore/add-circle-ci to master October 18, 2019 14:09
# Summary: Payment profiles store confidential payment information. Transactions can be processed against profiles.
# Docs: https://dev.na.bambora.com/docs/guides/payment_profiles/
# Endpoint: https://api.na.bambora.com/v1/profiles
def profiles; end
def profile
@profile ||= Bambora::Profile.new(self)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing the Client here so that the same client instance can be used for profiles, payment, etc.

@cassidycodes cassidycodes marked this pull request as ready for review October 18, 2019 14:37
@cassidycodes
Copy link
Contributor Author

end

def symbolize_keys(obj)
return obj unless obj.is_a?(Hash)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if you get a hash which holds a collection of hashes e.g.

obj = {
  'user' => {
    'accounts' => [
      {
        'number' => '123',
      },
      {
        'number' => '456',
      },
    ]
  }
}
def symbolize_keys(obj)
  return obj unless obj.is_a?(Hash)

  obj.each_with_object({}) do |(k, v), hash|
    hash[k.to_sym] = symbolize_keys(v)
    hash
  end
end

symbolize_keys(obj) => {:user=>{:accounts=>[{"number"=>"123"}, {"number"=>"456"}]}}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

took from rails source code:

def _deep_transform_keys_in_object(object, &block)
      case object
      when Hash
        object.each_with_object({}) do |(key, value), result|
          result[yield(key)] = _deep_transform_keys_in_object(value, &block)
        end
      when Array
        object.map { |e| _deep_transform_keys_in_object(e, &block) }
      else
        object
      end
    end

    _deep_transform_keys_in_object(obj){ |key| key.to_sym } => {:USER=>{:ACCOUNTS=>[{:NUMBER=>"123"}, {:NUMBER=>"456"}]}}

You could wrap that method into your method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks @bilogub!

def initialize(client)
super

@path = '/v1/profiles'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would probably start to break into api versions here and namespacing the classes into Bambora::V1:: in case users would want to stay on old version if supported and someone would develop V2 if available.

Copy link
Member

@harrylewis harrylewis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love watching this shape up, and I'm learning a lot from it. I had a few comments to consider.

@@ -7,10 +7,15 @@ PATH
GEM
remote: https://rubygems.org/
specs:
addressable (2.7.0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were these changes added manually? I don't see any changes made to the Gemfile.

Copy link
Contributor Author

@cassidycodes cassidycodes Oct 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are different ways of managing dependencies for gems. The "bundler way" is to list all dependencies in the gemspec then call gemspec in the Gemfile to load dependencies listed there.

https://bundler.io/v2.0/guides/creating_gem.html#testing-our-gem

I don't know much about this project, but hoe has another way of managing dependencies.

#### Create a Profile

```ruby
client.profile.create(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great example here!

README.md Show resolved Hide resolved
README.md Outdated
## Delete a Profile

```ruby
client.profile.delete(1234)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bambora remote customer codes contain letters, and are therefore strings. Use a string input instead for the example.

# frozen_string_literal: true

module Bambora
class Profile < JSONRequest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More of a semantic thing, I find it a bit confusing that Profile inherits from JSONRequest. I infer from this inheritance chain that Profile is a type of JSONRequest, when in reality it contains multiple methods that can make JSON requests. This seems to me to be more like a concern/module.

From the perspective of the caller though, when you are using this client, you are only ever using one API method at a time, so from the point of view of the caller, it does only represent one JSON request.

Now I've just argued with myself, haha.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to call this Domain since it represents the Bambora domain, but then I realized that the domain also uses XML. I could perhaps name this JSONDomain and XMLDomain.

request(method: :post, path: @path, body: card_data)
end

def delete(remote_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To better align with the Bambora terminology, call this parameter, customer_code. Also, could this be a named parameter so that is clear? When looking at the documentation, I had to go to the implementation to determine what the input was. A named parameter would clear up that confusion immediately.

Copy link
Member

@harrylewis harrylewis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-threading here we come!

README.md Outdated
$ gem install bambora-client
```bash
gem install bambora-client

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the extra space needed? Not a blocker for this PR. This could be fixed when some Markdown linting is included 👀.

@@ -50,20 +54,89 @@ client = Bambora::Client.new(
)
```

### Profiles

*Summary*: Payment profiles store confidential payment information. Transactions can be processed against profiles.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for cutting the line-length here.

end

def deep_transform_keys_in_object(object, &block)
case object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

spec/bambora/client_spec.rb Show resolved Hide resolved

def request(method:, path:, params: {}, body: {})
resp = client.request(method: method, path: path, params: params, body: body.to_json.to_s)
return parse_response(resp) if resp.status <= 300
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see here a problem - what if it is 404. As a json client i want to get a response with 404 and empty collection.
instead i get a generic error which i have to parse(raw text) to interpret it something meaningful in my app.
https://jsonplaceholder.typicode.com/posts/1 - this returns me an object and this returns me an empty object
https://jsonplaceholder.typicode.com/posts/12123123123 with a status in the response header.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bilogub Do you think we should parse all 4xx level errors and only raise an erro on 5xx and 3xx?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cassidycodes i think i gave a falsey option in my prev comment regading possible solution here and misled you, sorry.
In nutshell we should relay anything we get from api back to our client app(as long as we don't have our own API doc to rely on - we are only sitting in the middle between API and client app passing bytes there and back) - that said parse response in begin/rescue block and pass it up the chain. In case we end up in rescue block compose 50x server response in json format. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! I just pushed a change that passes a hash with the error back to the user.

@cassidycodes cassidycodes merged commit 2a2378d into master Oct 22, 2019
@cassidycodes cassidycodes deleted the feat/profiles-api branch October 22, 2019 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants