Skip to content

Commit

Permalink
Merge pull request #75 from OneBusAway/stripe
Browse files Browse the repository at this point in the history
Stripe Integration
  • Loading branch information
aaronbrethorst committed Nov 19, 2023
2 parents e1c2419 + c1f783d commit 4b4dfc0
Show file tree
Hide file tree
Showing 16 changed files with 234 additions and 21 deletions.
27 changes: 15 additions & 12 deletions .github/workflows/fly.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
name: Fly Deploy
name: Deploy to Fly.io

on:
push:
branches:
- main
push:
branches:
- main

env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
build-and-deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ruby-2.7.3
ruby-3.2
5 changes: 3 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
source 'https://rubygems.org'

ruby '2.7.3'
ruby '3.2'

gem 'rails', '~> 6.1.6'
gem 'pg', '~>1.5.0'
Expand All @@ -16,6 +16,7 @@ gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'bootstrap_form', "~>4.0.0"
gem 'local_time', '~>2.1.0'
gem 'stripe', '~>10.1.0'

# API
gem 'jbuilder', '~> 2.6'
Expand All @@ -37,7 +38,7 @@ gem 'bcrypt', '~> 3.1.12'
gem 'strip_attributes', '~>1.8.0'

# gem install nokogiri -v '1.8.4' -- --use-system-libraries --with-xml2-include=/usr/local/opt/libxml2/include/libxml2
gem 'nokogiri', '~> 1.13.6'
gem 'nokogiri', '~> 1.15.4'

gem 'pr_geohash', '~>1.0.0'

Expand Down
10 changes: 6 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ GEM
net-protocol
netrc (0.11.0)
nio4r (2.5.9)
nokogiri (1.13.10)
mini_portile2 (~> 2.8.0)
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
pg (1.5.4)
pr_geohash (1.0.0)
Expand Down Expand Up @@ -277,6 +277,7 @@ GEM
sprockets (>= 3.0.0)
strip_attributes (1.8.1)
activemodel (>= 3.0, < 7.0)
stripe (10.1.0)
temple (0.10.3)
thor (1.3.0)
tilt (2.3.0)
Expand Down Expand Up @@ -321,7 +322,7 @@ DEPENDENCIES
listen (~> 3.0.5)
local_time (~> 2.1.0)
mailgun-ruby (~> 1.1.10)
nokogiri (~> 1.13.6)
nokogiri (~> 1.15.4)
pg (~> 1.5.0)
pr_geohash (~> 1.0.0)
puma (~> 5.6.4)
Expand All @@ -339,14 +340,15 @@ DEPENDENCIES
spring
spring-watcher-listen (~> 2.0.0)
strip_attributes (~> 1.8.0)
stripe (~> 10.1.0)
uglifier (>= 1.3.0)
varint (~> 0.1.1)
vcr (~> 4.0.0)
web-console
webmock (~> 3.13.0)

RUBY VERSION
ruby 2.7.3p183
ruby 3.2.0p0

BUNDLED WITH
2.1.4
3 changes: 2 additions & 1 deletion Procfile.dev
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
web: ./bin/rails server
web: bundle exec rails server -p 3000
worker: bundle exec sidekiq -t 25 -C config/sidekiq.yml
19 changes: 19 additions & 0 deletions app/controllers/api/v1/payment_intents_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Api::V1::PaymentIntentsController < Api::V1::ApiController
skip_before_action :load_region

def create
if params[:donation_frequency] == 'recurring'
@recurring_response = Donations::Recurring.new(params[:donation_amount_in_cents]).run
@error = @recurring_response.error
else
@intent, @error = Donations::OneTime.new(params[:donation_amount_in_cents]).run
end

if @error
logger.error(@error.message)
head :internal_server_error
else
respond_to {|format| format.json}
end
end
end
28 changes: 28 additions & 0 deletions app/lib/donations/one_time.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Donations
class OneTime
def initialize(donation_amount_in_cents)
@donation_amount_in_cents = donation_amount_in_cents
end

def run
intent, error = [nil, nil]

begin
intent = Stripe::PaymentIntent.create(
{
amount: @donation_amount_in_cents,
currency: 'usd',
# In the latest version of the API, specifying the
# `automatic_payment_methods` parameter is optional
# because Stripe enables its functionality by default.
automatic_payment_methods: { enabled: true },
}
)
rescue Stripe::StripeError => e
error = e
end

[intent, error]
end
end
end
57 changes: 57 additions & 0 deletions app/lib/donations/recurring.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

module Donations
class Recurring
def initialize(donation_amount_in_cents)
@donation_amount_in_cents = donation_amount_in_cents
end

def run
recurring_response = RecurringResponse.new(@donation_amount_in_cents)

begin
customer = Stripe::Customer.create()
recurring_response.customer_id = customer.id

recurring_response.ephemeral_key = Stripe::EphemeralKey.create(
{customer: recurring_response.customer_id},
{stripe_version: '2023-08-16'}
)
recurring_response.subscription = create_subscription(recurring_response.customer_id)
rescue Stripe::StripeError => e
recurring_response.error = e
end

recurring_response
end

private

def create_subscription(customer_id)
# Create a new price for the custom donation amount
price = Stripe::Price.create(
unit_amount: @donation_amount_in_cents,
currency: 'usd',
recurring: { interval: 'month' },
product: $stripe_recurring_donation_product_id,
)

# Create or update the subscription with the new price
subscription = Stripe::Subscription.create(
customer: customer_id,
items: [{ price: price.id }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
)

subscription
end
end

class RecurringResponse
attr_accessor :donation_amount_in_cents, :subscription, :ephemeral_key, :error, :customer_id

def initialize(donation_amount_in_cents)
@donation_amount_in_cents = donation_amount_in_cents
end
end
end
9 changes: 9 additions & 0 deletions app/views/api/v1/payment_intents/create.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
if @recurring_response
json.client_secret(@recurring_response.subscription.latest_invoice.payment_intent.client_secret)
json.customer_id(@recurring_response.customer_id)
json.ephemeral_key(@recurring_response.ephemeral_key.secret)
else
json.client_secret(@intent.client_secret)
end

json.id(SecureRandom.uuid)
2 changes: 1 addition & 1 deletion config/credentials.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
MJwZKzEdAdLIxRzkIFbpFmuq/5oDLNqTtY+tYmjCvqxSBiO2TDQirH7As1nZ/ZAMAvyKzMI8134OtGvdq+Lem03Eqo74qNfJlKb6GUIqXbPRx4UaLEmSVSRaKQt5Uk3GUT+4f/SGh9ytlKGbKXWffEZGBfygMYMb+Vm301GMbLLOwusfF1p24luvBZC3dhrpUbsdaBLpZ3yJ6lnpZ9aP+A36nwwnbaeSAomlzvgKLPLyOGpx7XwevkhWxCNVoNJSBJTPlEeXXYVwR+tO9N9GqTotKAzcgY2On/HGkO/CY2qaul9aVxPGbxeS68CtEA8n3dj6OXv4TtykMgWCo5PxIYZempadDfT56lIyhZ/l4huhihElUYHSsdD7ztBGFAzsmOC1ncS35foYHF+JOlWbcVdqz8XFaZ1f+lWTyWwtIPPw/QsFUmEU8UKtzhrXt4+kyKF/WDIENa5AMMzSp52fB5qI8FqYKq/rkA/tGw4p4d1q0THxTaGMzqiAtQjZ5H33fMIeu1gYQ/uxlQ5aOkUZNTU2PcDTM+Kz6ezKbpYRGgYmeXrAqkIJQ/Jth1uXXON2P+5OitgZc58oYis6JLJdVrrDoKuVhq1K4jujQopuLZMsaH/dbCbJwkniEs8SaA5/Iiz6lnS3sDZgo/ZYA9wjZw/rUrjsGtOc9w==--DkBThg2KX7o0BrCe--yvrCQA4Puf8wp4WLCcNpuQ==
iK+va/I+Cg4UlUl1yXabH1CxYW4BuKuwDqBbtZEK0cAu9FuAik8b/sE+GsVcISaW6xjb19XeOWwgPZbx++Vfl8yNWRCpURkgSPyCmTa4Vqr8tif5jU0Osn9ttlLhrvtoqfDG0acQbbV0h8uiwkLTBGyULLzyQp/6ihdxNS79STAB7VEcBHlNHF/SqIMWPm+X6Y40bL8TtWDs8xXuF5qYnroTjffDqJBRngmPsSiFfddMw8jrZrsIgCYxwHeag/L2XKspeH9swMK4EyMmNs+TkxuY+i6TCQMDscIrgcg1nlHsV3nftHPSGW1sWVDdzhHkv/WJmC5DY483uGdnlrrmcMevIEDtz/lLySurKZjko51xcNxxJ4+tdTjg0wgY/134CqigEgbRpxNALUqLoPj8ABoAZO51njopDs9QAfvJD1fI3aZyZRcqEeRhKxWCSAGI6t4e2wSXL7K4F+aAO3fGYxrZt1Y71y9MGyQNdp3rSOsQChtdw1QV5BzUsCCdqFZzV6u+5gY0x2KfLnEu0WpYAEgvOBHCeWe7HOIXar5HTCP42jWg0PUxbLDpnKvLnl4HXcrlvFWo9OmBaw+1HUJcLdWqSEqVnh+l+T9/JQnXUVnpTz2Enus9JLntU8nISyOqHTOgGoSQqlXQijiB95SAf4Nu+gF9NKZiUkg/9wRr8w5zN6mgvSntH4vn2g+pWAuJEeD2Oop+j57j1atoXKFqIHf77FOSP9y0M5COw3IzOnRqZ/9hP4+FGwFB7xWqyd0VXHaIltzlIOJAC+Fm0q1X+P+pj7RM0PL+Vy/znG5OFiO+QhaIEfQfkuIQupqrRhiDUDoR2Yu4VUhALHQCF/9I4miP1K7nd8zTe2AKmspYUFdJ0nFzVPZ300jgex+B2irkFVwSiV1HkBE2gw3Ih26LdgtdrjMtmPJwK9Zgd3NxXE/WiT8ROD14CcZquJLCuGx+af8qfOr05j7CGHvob0xjqSiWEST59eaUWl+2JH7DMV73mCzzYub8+VVA9w==--XBBlGfnBwetha++2--tU5synzhrItLtVOfnR0mQw==
2 changes: 2 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.

config.hosts << "*.ngrok-free.app"

# In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
Expand Down
7 changes: 7 additions & 0 deletions config/initializers/stripe.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
if Rails.env.production?
Stripe.api_key = Rails.application.credentials.dig(:stripe_secret_key, :production)
$stripe_recurring_donation_product_id = "prod_OqlLl6mR66dLVQ"
else
Stripe.api_key = Rails.application.credentials.dig(:stripe_secret_key, :test)
$stripe_recurring_donation_product_id = "prod_P1xUtsgjEfkGgu"
end
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

namespace :api do
namespace :v1 do
# PaymentIntents (Stripe SDK)
resources :payment_intents, only: [:create]

resources :regions, only: [:index] do

get 'vehicles', on: :member, defaults: {format: 'json'}
Expand Down
Empty file.
55 changes: 55 additions & 0 deletions spec/lib/donations/recurring_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
require 'rails_helper'

describe "Donations::Recurring" do
describe '#initialize' do
it 'sets the donation amount' do
donation = Donations::Recurring.new(500)
expect(donation.instance_variable_get(:@donation_amount_in_cents)).to eq(500)
end
end

describe '#run' do
before do
allow(Stripe::Customer).to receive(:create).and_return(double('Customer', id: 'cus_test'))
allow(Stripe::EphemeralKey).to receive(:create).and_return(double('EphemeralKey'))
allow_any_instance_of(Donations::Recurring).to receive(:create_subscription).and_return(double('Subscription'))
end

it 'creates a new Stripe customer' do
donation = Donations::Recurring.new(500)
donation.run
expect(Stripe::Customer).to have_received(:create)
end

it 'creates a new Stripe ephemeral key' do
donation = Donations::Recurring.new(500)
donation.run
expect(Stripe::EphemeralKey).to have_received(:create)
end

it 'creates or finds a subscription' do
donation = Donations::Recurring.new(500)
donation.run
expect(donation).to have_received(:create_subscription)
end
end

describe '#create_subscription' do
before do
allow(Stripe::Price).to receive(:create).and_return(double('Price', id: 'price_test'))
allow(Stripe::Subscription).to receive(:create).and_return(double('Subscription'))
end

it 'creates a new Stripe price' do
donation = Donations::Recurring.new(500)
donation.send(:create_subscription, 'cus_test')
expect(Stripe::Price).to have_received(:create)
end

it 'creates a new Stripe subscription' do
donation = Donations::Recurring.new(500)
donation.send(:create_subscription, 'cus_test')
expect(Stripe::Subscription).to have_received(:create)
end
end
end
26 changes: 26 additions & 0 deletions spec/requests/api/v1/payment_intents_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'rails_helper'

RSpec.describe "Api::V1::PaymentIntents", type: :request do
let(:donation_amount_in_cents) { 1000 }

describe "POST /create" do
it "creates a new PaymentIntent" do
allow(Stripe::PaymentIntent).to receive(:create).and_return(OpenStruct.new(client_secret: "12435"))
post api_v1_payment_intents_path(format: "json"), params: { donation_amount_in_cents: donation_amount_in_cents }
expect(response).to be_successful
expect(response.body).to include("12435")
end

context "when there is a Stripe error" do
before do
allow(Stripe::PaymentIntent).to receive(:create).and_raise(Stripe::StripeError.new)
end

it "returns a 500 status" do
post api_v1_payment_intents_path, params: { donation_amount_in_cents: donation_amount_in_cents }

expect(response).to have_http_status(:internal_server_error)
end
end
end
end

0 comments on commit 4b4dfc0

Please sign in to comment.