Skip to content

Commit

Permalink
Stripe checkout & WeChat Pay
Browse files Browse the repository at this point in the history
This patch introduces a new required environment variable.
It also requires production  to have Stripe webhooks configured
against the `checkout.session.completed` and `checkout.session.expired`
events. That configuration should be completed prior to deploy.

For production, webhooks can be configured using:
https://dashboard.stripe.com/webhooks

The webhook will need to point to https://PRODUCTION_HOST/stripe_webhook
Webhook events are opt-in, so make sure to include both
`checkout.session.completed` and `checkout.session.expired`.

After configuring the webhook, you'll want to shove its signing secret
into the STRIPE_WEBHOOK_ENDPOINT_SECRET environment variable.

For local testing, you can test Stripe webhooks using the Stripe CLI
and the `stripe listen` command.
  • Loading branch information
Betsy Haibel committed Oct 21, 2021
1 parent b56bd59 commit fdcacdc
Show file tree
Hide file tree
Showing 14 changed files with 412 additions and 224 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ HOSTNAME=localhost:3000
# Generate them here https://dashboard.stripe.com/account/apikeys
STRIPE_PUBLIC_KEY=pk_test_zq022DcopypastatXAVMaOJT
STRIPE_PRIVATE_KEY=sk_test_35SiP3qovcopypastaLguIyY
# Stripe webhook secret
# Use the Stripe CLI and `stripe listen --forward-to localhost:3000/stripe_webhook` to configure this in development.
# For production configuration, go to https://dashboard.stripe.com/webhooks
STRIPE_WEBHOOK_ENDPOINT_SECRET=whsec_HcopypastaS7IH3D779S
# https://stripe.com/docs/currencies
STRIPE_CURRENCY=NZD

Expand Down
132 changes: 0 additions & 132 deletions app/commands/money/charge_customer.rb

This file was deleted.

134 changes: 134 additions & 0 deletions app/commands/money/start_stripe_checkout.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# frozen_string_literal: true

# Copyright 2019 Matthew B. Gray
# Copyright 2019 AJ Esler
# Copyright 2021 DisCon III
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Money::StartStripeCheckout creates a pending charge record against a User for the Stripe integrations
# and sets them up to check out with Stripe.
# Truthy returns mean the user can be safely sent on to the checkout flow, otherwise check #errors for failure details.
class Money::StartStripeCheckout
attr_reader :reservation, :user, :charge_amount, :charge, :amount_owed, :success_url, :cancel_url

def initialize(reservation:, user:, amount_owed:, success_url:, cancel_url:, charge_amount: nil)
@reservation = reservation
@user = user
@charge_amount = charge_amount || amount_owed
@amount_owed = amount_owed
@success_url = success_url
@cancel_url = cancel_url
end

def call
setup_stripe_customer

check_charge_amount unless errors.any?
create_checkout_session unless errors.any?

charge_state_params = if errors.any?
{
state: ::Charge::STATE_FAILED,
comment: error_message,
}
else
{
state: ::Charge::STATE_PENDING,
comment: "Pending stripe payment",
}
end

@charge = ::Charge.stripe.create!({
user: user,
reservation: reservation,
stripe_id: @checkout_session.id,
amount: charge_amount,
}.merge(charge_state_params))

checkout_url.present?
end

def error_message
errors.to_sentence
end

def errors
@errors ||= []
end

def checkout_url
@checkout_session['url']
end

private

def check_charge_amount
if !charge_amount.present?
errors << "charge amount is missing"
end
if charge_amount <= 0
errors << "amount must be more than 0 cents"
end
if charge_amount > amount_owed
errors << "refusing to overpay for reservation"
end
end

def create_checkout_session
@checkout_session = Stripe::Checkout::Session.create({
line_items: [
{
currency: ENV.fetch('STRIPE_CURRENCY'),
amount: charge_amount.cents,
name: reservation.membership.to_s,
quantity: 1,
},
],
payment_method_types: [
'card',
'wechat_pay',
],
payment_method_options: {
wechat_pay: {
client: 'web',
}
},
mode: 'payment',
customer: user.stripe_id,
success_url: success_url,
cancel_url: cancel_url,
})
end

def setup_stripe_customer
if !user.stripe_id.present?
stripe_customer = Stripe::Customer.create(email: user.email)
user.update!(stripe_id: stripe_customer.id)
end
rescue Stripe::StripeError => e
errors << e.message.to_s
@charge.stripe_response = json_to_hash(e.response)
@charge.comment = "Failed to setup customer - #{e.message}"
end

def json_to_hash(obj)
JSON.parse(obj.to_json)
rescue
{}
end

def fully_paid?
@charge.successful? && (amount_owed - charge_amount) <= 0
end
end
54 changes: 54 additions & 0 deletions app/commands/money/stripe_checkout_failed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

# Copyright 2021 DisCon III
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Money::StripeCheckoutFailed updates the Charge record associated with that checkout session to indicate that the payment failed.
# Truthy returns mean that everything updated correctly, otherwise check #errors for failure details.
class Money::StripeCheckoutFailed
attr_reader :charge, :stripe_checkout_session

def initialize(charge:, stripe_checkout_session:)
@charge = charge
@stripe_checkout_session = stripe_checkout_session
end

def call
reservation = charge.reservation
reservation.transaction do
charge.state = ::Charge::STATE_FAILED
charge.stripe_response = json_to_hash(stripe_checkout_session)
charge.comment = "Stripe checkout failed."
charge.save!
end

return charge
end

def error_message
errors.to_sentence
end

def errors
@errors ||= []
end

private

def json_to_hash(obj)
JSON.parse(obj.to_json)
rescue
{}
end
end
Loading

0 comments on commit fdcacdc

Please sign in to comment.