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

Stripe connect #1468

Merged
merged 184 commits into from
Sep 27, 2017
Merged

Stripe connect #1468

merged 184 commits into from
Sep 27, 2017

Conversation

oeoeaio
Copy link
Contributor

@oeoeaio oeoeaio commented Feb 23, 2017

Work by @stveep to connect Stripe Accounts to the main instance, and by me to implement a new Stripe Connect based payment gateway.

I figure that if this all works, we can pull it in now, and then pull the rest of it in later...(#1466 and #1456)

@oeoeaio oeoeaio mentioned this pull request Feb 23, 2017
@oeoeaio oeoeaio force-pushed the stripe-connect branch 2 times, most recently from 9183360 to b58381c Compare May 31, 2017 05:48
clear_ship_address
binding.pry
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved


def construct_saved_card_attributes
existing_card_id = params[:order].delete(:existing_card)
if existing_card_id.present?
Copy link
Contributor

Choose a reason for hiding this comment

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

early return maybe? return unless existing_card_id.present? or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved


def set_user
@user = spree_current_user
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this? spree_current_user should be memoized already, right? just use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

else
payment.send(:gateway_error, response.message)
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

This method seems too long to me, I see the opportunity to split it.

Copy link
Contributor

Choose a reason for hiding this comment

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

It might be a CreateProfile on its own.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

# Assume the gateway_payment_profile_id is a token generated by StripeJS
return token_or_card_id
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Conditionals with assignments could lead to weird outcomes in ruby. Try to avoid them if possible, furthermore they make the code less readable IMHO.

Copy link
Contributor

Choose a reason for hiding this comment

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

What's your preferred pattern to check whether something exists and then use it if it does?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

token = Stripe::Token.create({card: card, customer: customer}, { stripe_account: stripe_account_id})
token.id
rescue Stripe::InvalidRequestError
# Not really sure what to do here....
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe at least log it in stdout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

if !(spree_current_user.enterprises.include? @enterprise) && !(spree_current_user.admin?)
deauthorize_request_for_stripe_id(response_params["stripe_user_id"])
redirect_to '/unauthorized' and return
end
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that lines from 123 until here are business logic that would be better off in a factory method of StripeAccount at least, or even on a different object.

As I see it, we could set up error handling here in the controller to deal with these two exceptions:

def stripe_connect_callback
  stripe_account = StripeAccountFactory.new(params)
  if stripe_account.save
    ...
  end
rescue Stripe::Error
  redirect_to '/unauthorized' and return
end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

@@ -10,14 +10,14 @@ class EnterprisesController < BaseController
before_filter :check_stock_levels, only: :shop

before_filter :clean_permalink, only: :check_permalink
before_filter :set_enterprise, only: :relatives
Copy link
Contributor

Choose a reason for hiding this comment

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

why don't you call the method from the controller action? I believe single-action before_filters have little to no value. It is easier for the reader to see a method call when reading the action than jumping between the before_filter and the action.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

@@ -37,8 +37,14 @@ def check_permalink
end
end



Copy link
Contributor

@sauloperez sauloperez Jun 8, 2017

Choose a reason for hiding this comment

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

⚠️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

class CreditCardsController < BaseController

before_filter :set_credit_card, only: [:destroy]
before_filter :destroy_at_stripe, only: [:destroy]
Copy link
Contributor

Choose a reason for hiding this comment

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

related to my comment in enterprises_controller.rb

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

def destroy
if @credit_card.destroy
flash[:success] = I18n.t(:card_has_been_removed, number: "x-#{@credit_card.last_digits}")
redirect_to "/account#/cards"
Copy link
Contributor

Choose a reason for hiding this comment

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

Using url helpers here would make it future-proof. In case someday we need to change the route we don't have to update this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

@@ -1,3 +1,20 @@
Spree::UsersController.class_eval do
layout 'darkswarm'
before_filter :set_credit_card, only: :show
Copy link
Contributor

Choose a reason for hiding this comment

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

See my suggestion in enterprises_controller.rb

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

JWT.decode(token, Openfoodnetwork::Application.config.secret_token, true, algorithm: 'HS256')[0] # only returns the original payload
end
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

I stronlgy believe this module should actually be the abstraction the controllers are lacking. For my experience, I believe that payment gateways need to be encapsulated so as not to leak details throughout the whole app. I see that these methods could be independent service objects like Stripe::Authorize, Stripe::Deauthorize, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

@@ -365,6 +366,7 @@ def can_invoice?
abn.present?
end


Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

else
payment.send(:gateway_error, response.message)
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be a CreateProfile on its own.

@oeoeaio oeoeaio force-pushed the stripe-connect branch 4 times, most recently from 2329e75 to 7c311ff Compare June 22, 2017 05:22
@sauloperez
Copy link
Contributor

Feel free to dismiss my comments. I know I came late to the party and I don't want to block the PR.

@oeoeaio
Copy link
Contributor Author

oeoeaio commented Jun 22, 2017

@sauloperez, no way. I will definitely be looking at them!

@oeoeaio oeoeaio force-pushed the stripe-connect branch 2 times, most recently from 305f8fa to 1839a0c Compare July 7, 2017 06:06
@@ -34,7 +34,7 @@ def unauthorized
redirect_to '/unauthorized'
else
store_location
redirect_to root_path(anchor: "login?after_login=#{ request.env['PATH_INFO'] }")
redirect_to root_path(anchor: "login?after_login=#{request.env['PATH_INFO']}")
end
end

Copy link
Contributor

Choose a reason for hiding this comment

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

This reminds me - I had problems with getting signed out of OFN when going through the Stripe Connect process to connect an account. When I was redirected back to OFN I got prompted to sign in, but was not then redirected to the admin page (and the account was not connected on the OFN side as the request was not successful). I only experienced it locally and I'm not sure what the reason for getting signed out was. I remember playing with using some of the other request params here instead (e.g. #fullpath), but was worried I was going to break everything...

stveep@9f3888c

angular.extend(this, new FieldsetMixin($scope))
defaultCard = [ {id: null, formatted: t("new_credit_card")} ]
$scope.savedCreditCards = defaultCard.concat savedCreditCards if savedCreditCards
Copy link
Contributor

Choose a reason for hiding this comment

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

you can merge these two objects using Object.assign without having to add the conditional, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

@@ -53,6 +59,20 @@ Darkswarm.factory 'Checkout', (CurrentOrder, ShippingMethods, PaymentMethods, $h
last_name: @order.bill_address.lastname
}

if @paymentMethod()?.method_type == 'stripe'
if @secrets.selected_card
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd rather use an and here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry @sauloperez, I don't follow. You mean instead of the ?. syntax?

private

def load_settings
klass = Struct.new(:stripe_connect_enabled)
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no need to create the Struct on every request. You can assign it to a variable like StripeSettings = Struct.new(:stripe_connect_enabled) just next to the before_filter above and use in this line below. As it is, it has an impact on performance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

@@ -201,4 +204,29 @@ def redirect_to_paypal_express_form_if_needed
render json: {path: spree.paypal_express_url(payment_method_id: payment_method.id)}, status: 200
true
end

def construct_saved_card_attributes
existing_card_id = params[:order].delete(:existing_card)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd rename the key in the sent JSON to existing_card_id for the sake of consistency. It's good to know at a glance whether we're working with an integer or a string

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

Copy link
Contributor Author

@oeoeaio oeoeaio Sep 11, 2017

Choose a reason for hiding this comment

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

For reference this was written the way it was because existing_card is the key used by future versions of Spree. We are definitely going to have to deal with lots of other stuff between here and there however, so let's use existing_card_id for now.

return if existing_card_id.blank?

credit_card = Spree::CreditCard.find(existing_card_id)
if credit_card.try(:user_id).blank? || credit_card.user_id != spree_current_user.try(:id)
Copy link
Contributor

Choose a reason for hiding this comment

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

how come a credit card is not associated to a user? I would rather enforce it with validations so no credit card is added to the system without a user. This way we don't need to check this every time we need to use the credit card.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sauloperez Users who check out as a guest can still pay via credit card.

class StripeAccount < ActiveRecord::Base
belongs_to :enterprise
validates :stripe_user_id, :stripe_publishable_key, presence: true
validates :enterprise_id, uniqueness: true
Copy link
Contributor

Choose a reason for hiding this comment

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

we must enforce this constrain at db level as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

end
end

module Api::Admin::PaymentMethod
Copy link
Contributor

Choose a reason for hiding this comment

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

I would move this to its own file for consistency. Future devs might expect finding the base serializer in the app/serializers/api/admin/ directory like all the others.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

end

def tags
object.tag_list.map{ |t| { text: t } }
class StripeSerializer < BaseSerializer
Copy link
Contributor

Choose a reason for hiding this comment

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

here as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

private

def state
OAuth.send(:jwt_decode, params["state"])
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't we make jwt_decode public if we need to use it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved


it "returns a 500 error" do
spree_get :stripe_connect_callback, params
response.status.should be 500
Copy link
Contributor

Choose a reason for hiding this comment

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

please, we should use the current expect(...).to eq() syntax 🙏

Copy link
Contributor Author

@oeoeaio oeoeaio Sep 11, 2017

Choose a reason for hiding this comment

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

Now in: spec/controllers/admin/stripe_accounts_controller_spec.rb

Resolved

end

def update
Spree::Config.set(params[:settings])
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please explain and document this?
What do you expect in params[:settings]?
Is Spree::Config.set() really needed? It will get cached by Rails and affect other requests (and specs).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updating the cached config is exactly what we're trying to achieve here. This controller action is used by super admin users to update the settings the app is using.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah OK, could you please specify that in the method documentation? Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

@@ -39,6 +39,10 @@ def check_permalink

private

def set_enterprise
@enterprise = Enterprise.find(params[:id])
Copy link
Contributor

Choose a reason for hiding this comment

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

what if the enterprise does not exist? do we need any authorization ability here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@enricostano I have changed this to Enterprise.find_by_id so that an error isn't raised when a bad id is provided. This method is only used by EnterprisesController#relatives, logic there already deals with the case that no enterprise is found.

No authorisation required here IMO, as this is just describing the existence of relationships in the network. I think this is fine to be public knowledge.

# current payment_method is already a Stripe method
def show_stripe?
Spree::Config.stripe_connect_enabled || @payment_method.try(:type) == "Spree::Gateway::StripeConnect"
end
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ DISCLAIMER: buzz words ahead ⚠️

I think there is room here to apply some "tell don't ask", instead of asking all the time if we support Stripe we should just get that info from some class defined starting up the app (strategy pattern maybe?). In that way you'll not have to remember to check that "if Stripe" in the future avoiding bugs and letting the code be more clear and easier to test.

BTW, this is not blocking of course. Just something that would be nice to start seeing in the codebase.

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 value of Spree::Config.stripe_connect_enabled is dynamic (can be changed by super admin), so we can't cache the value on app initialisation. Is that what you mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved?

end

def set_credit_card
@credit_card = Spree::CreditCard.find(params[:id])
Copy link
Contributor

Choose a reason for hiding this comment

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

What if the card doesn't exist? Any authorization layer needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved


# def cancel(response_code)
# provider.void(response_code, {})
# end
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we remove commented out code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

options = {
email: payment.order.email,
login: Stripe.api_key,
}.merge! address_for(payment)
Copy link
Contributor

Choose a reason for hiding this comment

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

Not super important, but if you move this hash population to a separate method you'll have more documentation surface and we'll get prepared for the rubocop rules about methods and classes length I hope we'll agree upon some day 😬

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved

@oeoeaio oeoeaio force-pushed the stripe-connect branch 2 times, most recently from 842d937 to 7ce7cab Compare July 12, 2017 08:18
return if source_attributes.nil?
if payment_method and payment_method.payment_source_class
self.source = payment_method.payment_source_class.new(source_attributes)
self.source.payment_method_id = payment_method.id
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey @stveep, I am a bit worried about this line. I don't think it is going to work for us to link instances Spree::CreditCard to a single Spree::PaymentMethod - this will imply that each card can only be used by a single shop, and we want cards to be used by multiple shops.... Was there a particular reason that we need this association as far as you're aware? I realise this method is coming from Spree, so they must have a reason but I suspect it is more for convenience than actual necessity, because the payment itself stores the payment method, so I don't see why the card needs to store it as well...

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe @sauloperez can help here too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Bump @stveep

Make sure that completed payment is for an amount less that the order total
Connector now handles cancellation of the connection wizard by the user more elegantly
This step is not being responded to anyway, since we are not rending a page for each checkout step

It was causing an issue whereby an order in the 'confirm' state was not able to progress through the
checkout controller because it was expecting to only redirect to paypal from the 'payment' state.
figured it was easiest to just remove the step, seeing as it wasn't being used in any meaningful way.
It should be fine to bring the 'confirm' step back in the future if we need it, we will just have to
make sure paypal the paypal issue is resolved.
@RohanM RohanM merged commit 75ec77d into openfoodfoundation:master Sep 27, 2017
@mkllnk mkllnk requested a deployment to staging September 27, 2017 01:07 Abandoned
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants