-
-
Notifications
You must be signed in to change notification settings - Fork 729
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
Stripe connect #1468
Conversation
9183360
to
b58381c
Compare
clear_ship_address | ||
binding.pry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.... |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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_filter
s 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.
There was a problem hiding this comment.
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 | |||
|
|||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
app/helpers/admin/stripe_helper.rb
Outdated
JWT.decode(token, Openfoodnetwork::Application.config.secret_token, true, algorithm: 'HS256')[0] # only returns the original payload | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
app/models/enterprise.rb
Outdated
@@ -365,6 +366,7 @@ def can_invoice? | |||
abn.present? | |||
end | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
2329e75
to
7c311ff
Compare
Feel free to dismiss my comments. I know I came late to the party and I don't want to block the PR. |
@sauloperez, no way. I will definitely be looking at them! |
305f8fa
to
1839a0c
Compare
@@ -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 | |||
|
There was a problem hiding this comment.
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...
angular.extend(this, new FieldsetMixin($scope)) | ||
defaultCard = [ {id: null, formatted: t("new_credit_card")} ] | ||
$scope.savedCreditCards = defaultCard.concat savedCreditCards if savedCreditCards |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
lib/stripe/account_connector.rb
Outdated
private | ||
|
||
def state | ||
OAuth.send(:jwt_decode, params["state"]) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 🙏
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
842d937
to
7ce7cab
Compare
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 |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump @stveep
919a45a
to
ccafc8f
Compare
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
…ccount from displaying in the checkout
Log deauthorise failures to Bugsnag
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.
544a847
to
75ec77d
Compare
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)