├── log └── .keep ├── tmp └── .keep ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── user_test.rb │ ├── charge_test.rb │ ├── courses_test.rb │ └── stripe_account_test.rb ├── controllers │ ├── .keep │ ├── disputes_controller_test.rb │ ├── payouts_controller_test.rb │ ├── debit_cards_controller_test.rb │ ├── charges_controller_test.rb │ ├── campaigns_controller_test.rb │ ├── bank_accounts_controller_test.rb │ └── stripe_accounts_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── campaigns.yml │ ├── charges.yml │ ├── stripe_accounts.yml │ └── users.yml ├── integration │ └── .keep └── test_helper.rb ├── .ruby-version ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── sf_sm.jpg │ │ ├── desk_lg.jpg │ │ ├── desk_sm.jpg │ │ ├── favicon.ico │ │ ├── sf_lg.jpeg │ │ ├── snow_sm.jpg │ │ ├── beach_lg.jpeg │ │ ├── beach_sm.jpg │ │ ├── money_lg.jpg │ │ ├── money_sm.jpg │ │ ├── ocean_lg.jpg │ │ ├── ocean_sm.jpeg │ │ ├── puppy_lg.jpeg │ │ ├── puppy_sm.jpg │ │ ├── snow_lg.jpeg │ │ ├── travel_lg.jpeg │ │ ├── travel_sm.jpg │ │ ├── wedding_sm.jpg │ │ └── wedding_lg.jpeg │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── campaigns.js │ │ ├── cable.js │ │ └── application.js │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ ├── application.css │ │ ├── components.scss │ │ └── custom.scss ├── models │ ├── concerns │ │ └── .keep │ ├── charge.rb │ ├── application_record.rb │ ├── user.rb │ ├── campaign.rb │ └── stripe_account.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── pages_controller.rb │ ├── registrations_controller.rb │ ├── application_controller.rb │ ├── payouts_controller.rb │ ├── disputes_controller.rb │ ├── bank_accounts_controller.rb │ ├── debit_cards_controller.rb │ ├── webhooks_controller.rb │ ├── charges_controller.rb │ ├── campaigns_controller.rb │ └── stripe_accounts_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ ├── _messages.html.erb │ │ ├── _footer.html.erb │ │ ├── application.html.erb │ │ └── _header.html.erb │ ├── kaminari │ │ ├── _gap.html.erb │ │ ├── _first_page.html.erb │ │ ├── _last_page.html.erb │ │ ├── _next_page.html.erb │ │ ├── _prev_page.html.erb │ │ ├── _page.html.erb │ │ └── _paginator.html.erb │ ├── devise │ │ ├── mailer │ │ │ ├── password_change.html.erb │ │ │ ├── confirmation_instructions.html.erb │ │ │ ├── unlock_instructions.html.erb │ │ │ └── reset_password_instructions.html.erb │ │ ├── unlocks │ │ │ └── new.html.erb │ │ ├── confirmations │ │ │ └── new.html.erb │ │ ├── passwords │ │ │ ├── edit.html.erb │ │ │ └── new.html.erb │ │ ├── shared │ │ │ └── _links.html.erb │ │ ├── sessions │ │ │ └── new.html.erb │ │ └── registrations │ │ │ ├── new.html.erb │ │ │ └── edit.html.erb │ ├── admin │ │ └── kaminari │ │ │ ├── _gap.html.erb │ │ │ ├── _last_page.html.erb │ │ │ ├── _first_page.html.erb │ │ │ ├── _next_page.html.erb │ │ │ ├── _prev_page.html.erb │ │ │ ├── _page.html.erb │ │ │ └── _paginator.html.erb │ ├── stripe_accounts │ │ ├── new.html.erb │ │ ├── edit.html.erb │ │ └── _account_form.html.erb │ ├── campaigns │ │ ├── _verification.html.erb │ │ ├── _donations.html.erb │ │ ├── _transactions.html.erb │ │ ├── _checkoutform.html.erb │ │ ├── _campaigns.html.erb │ │ ├── _transfers.html.erb │ │ ├── _paymentform.html.erb │ │ ├── edit.html.erb │ │ ├── home.html.erb │ │ ├── _images.html.erb │ │ ├── new.html.erb │ │ ├── dashboard.html.erb │ │ ├── _charges.html.erb │ │ └── show.html.erb │ ├── pages │ │ ├── terms.html.erb │ │ └── pricing.html.erb │ ├── debit_cards │ │ ├── _debit_card_form.html.erb │ │ └── new.html.erb │ ├── bank_accounts │ │ ├── _bank_account_form.html.erb │ │ └── new.html.erb │ ├── payouts │ │ └── show.html.erb │ └── charges │ │ ├── _dispute.html.erb │ │ └── show.html.erb ├── helpers │ ├── charges_helper.rb │ ├── payouts_helper.rb │ ├── disputes_helper.rb │ ├── bank_accounts_helper.rb │ ├── debit_cards_helper.rb │ ├── application_helper.rb │ ├── devise_helper.rb │ └── stripe_accounts_helper.rb ├── jobs │ └── application_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb └── mailers │ └── application_mailer.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── bin ├── bundle ├── rake ├── rails ├── spring ├── update └── setup ├── config ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── initializers │ ├── session_store.rb │ ├── mime_types.rb │ ├── stripe.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── cookies_serializer.rb │ ├── kaminari_config.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ └── new_framework_defaults.rb ├── application.rb ├── database.yml ├── locales │ ├── en.yml │ └── devise.en.yml ├── routes.rb ├── secrets.yml ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb └── puma.rb ├── config.ru ├── db ├── migrate │ ├── 20160911175135_add_image_to_course.rb │ ├── 20161228180341_add_name_to_charges.rb │ ├── 20160910213144_rename_lessons_to_courses.rb │ ├── 20161227183231_add_goal_to_campaigns.rb │ ├── 20161228182556_add_raised_to_campaign.rb │ ├── 20161227175740_rename_courses_to_campaigns.rb │ ├── 20160911185541_add_stripe_account_to_users.rb │ ├── 20160915051609_add_content_link_to_courses.rb │ ├── 20161228174458_remove_user_id_from_charges.rb │ ├── 20160915051353_change_description_in_courses.rb │ ├── 20161228223505_add_active_to_campaign.rb │ ├── 20170502170520_add_acct_id_to_stripe_accounts.rb │ ├── 20161227180421_modify_campaigns_and_charges.rb │ ├── 20160910162519_create_lessons.rb │ ├── 20160913025720_create_charges.rb │ ├── 20160911193440_create_stripe_accounts.rb │ ├── 20160911230223_add_fields_to_stripe_account.rb │ ├── 20170405165023_add_more_fields_to_stripe_accounts.rb │ └── 20160910151553_devise_create_users.rb ├── seeds.rb └── schema.rb ├── Rakefile ├── .gitignore ├── Gemfile ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.5 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/helpers/charges_helper.rb: -------------------------------------------------------------------------------- 1 | module ChargesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/payouts_helper.rb: -------------------------------------------------------------------------------- 1 | module PayoutsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/disputes_helper.rb: -------------------------------------------------------------------------------- 1 | module DisputesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/bank_accounts_helper.rb: -------------------------------------------------------------------------------- 1 | module BankAccountsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/debit_cards_helper.rb: -------------------------------------------------------------------------------- 1 | module DebitCardsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/models/charge.rb: -------------------------------------------------------------------------------- 1 | class Charge < ApplicationRecord 2 | 3 | end 4 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/fixtures/campaigns.yml: -------------------------------------------------------------------------------- 1 | one: 2 | id: 1 3 | user_id: 1 4 | title: Title 5 | description: Description -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | class PagesController < ApplicationController 2 | def pricing 3 | end 4 | end -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/images/sf_sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/sf_sm.jpg -------------------------------------------------------------------------------- /app/views/kaminari/_gap.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= content_tag :a, raw(t 'views.pagination.truncate') %> 3 |
  • 4 | -------------------------------------------------------------------------------- /app/assets/images/desk_lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/desk_lg.jpg -------------------------------------------------------------------------------- /app/assets/images/desk_sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/desk_sm.jpg -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/sf_lg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/sf_lg.jpeg -------------------------------------------------------------------------------- /app/assets/images/snow_sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/snow_sm.jpg -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/beach_lg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/beach_lg.jpeg -------------------------------------------------------------------------------- /app/assets/images/beach_sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/beach_sm.jpg -------------------------------------------------------------------------------- /app/assets/images/money_lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/money_lg.jpg -------------------------------------------------------------------------------- /app/assets/images/money_sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/money_sm.jpg -------------------------------------------------------------------------------- /app/assets/images/ocean_lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/ocean_lg.jpg -------------------------------------------------------------------------------- /app/assets/images/ocean_sm.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/ocean_sm.jpeg -------------------------------------------------------------------------------- /app/assets/images/puppy_lg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/puppy_lg.jpeg -------------------------------------------------------------------------------- /app/assets/images/puppy_sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/puppy_sm.jpg -------------------------------------------------------------------------------- /app/assets/images/snow_lg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/snow_lg.jpeg -------------------------------------------------------------------------------- /app/assets/images/travel_lg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/travel_lg.jpeg -------------------------------------------------------------------------------- /app/assets/images/travel_sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/travel_sm.jpg -------------------------------------------------------------------------------- /app/assets/images/wedding_sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/wedding_sm.jpg -------------------------------------------------------------------------------- /app/assets/images/wedding_lg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamjstevenson/stripe-connect-managed-rails/HEAD/app/assets/images/wedding_lg.jpeg -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /test/controllers/disputes_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DisputesControllerTest < ActionDispatch::IntegrationTest 4 | 5 | end 6 | -------------------------------------------------------------------------------- /app/views/kaminari/_first_page.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote %> 3 |
  • 4 | -------------------------------------------------------------------------------- /app/views/kaminari/_last_page.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} %> 3 |
  • 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/devise/mailer/password_change.html.erb: -------------------------------------------------------------------------------- 1 |

    Hello <%= @resource.email %>!

    2 | 3 |

    We're contacting you to notify you that your password has been changed.

    4 | -------------------------------------------------------------------------------- /app/views/kaminari/_next_page.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote %> 3 |
  • 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | -------------------------------------------------------------------------------- /test/models/charge_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ChargeTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/courses_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class LessonTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/kaminari/_prev_page.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote %> 3 |
  • 4 | -------------------------------------------------------------------------------- /db/migrate/20160911175135_add_image_to_course.rb: -------------------------------------------------------------------------------- 1 | class AddImageToCourse < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :courses, :image, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161228180341_add_name_to_charges.rb: -------------------------------------------------------------------------------- 1 | class AddNameToCharges < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :charges, :name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160910213144_rename_lessons_to_courses.rb: -------------------------------------------------------------------------------- 1 | class RenameLessonsToCourses < ActiveRecord::Migration[5.0] 2 | def change 3 | rename_table :lessons, :courses 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161227183231_add_goal_to_campaigns.rb: -------------------------------------------------------------------------------- 1 | class AddGoalToCampaigns < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :campaigns, :goal, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/models/stripe_account_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StripeAccountTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_seller-dashboard_session' 4 | -------------------------------------------------------------------------------- /db/migrate/20161228182556_add_raised_to_campaign.rb: -------------------------------------------------------------------------------- 1 | class AddRaisedToCampaign < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :campaigns, :raised, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161227175740_rename_courses_to_campaigns.rb: -------------------------------------------------------------------------------- 1 | class RenameCoursesToCampaigns < ActiveRecord::Migration[5.0] 2 | def change 3 | rename_table :courses, :campaigns 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /db/migrate/20160911185541_add_stripe_account_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddStripeAccountToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :stripe_account, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160915051609_add_content_link_to_courses.rb: -------------------------------------------------------------------------------- 1 | class AddContentLinkToCourses < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :courses, :content_link, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161228174458_remove_user_id_from_charges.rb: -------------------------------------------------------------------------------- 1 | class RemoveUserIdFromCharges < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :charges, :user_id, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160915051353_change_description_in_courses.rb: -------------------------------------------------------------------------------- 1 | class ChangeDescriptionInCourses < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column :courses, :description, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161228223505_add_active_to_campaign.rb: -------------------------------------------------------------------------------- 1 | class AddActiveToCampaign < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :campaigns, :active, :boolean, default: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | class RegistrationsController < Devise::RegistrationsController 2 | protected 3 | 4 | def after_sign_up_path_for(resource) 5 | new_campaign_path 6 | end 7 | end -------------------------------------------------------------------------------- /db/migrate/20170502170520_add_acct_id_to_stripe_accounts.rb: -------------------------------------------------------------------------------- 1 | class AddAcctIdToStripeAccounts < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :stripe_accounts, :acct_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/stripe.rb: -------------------------------------------------------------------------------- 1 | Rails.configuration.stripe = { 2 | :publishable_key => ENV['PUBLISHABLE_KEY'], 3 | :secret_key => ENV['SECRET_KEY'] 4 | } 5 | 6 | Stripe.api_key = Rails.configuration.stripe[:secret_key] 7 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/campaigns.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $(".campaign-image").click(function(){ 3 | $(".img-thumbnail").removeClass("img-selected"); 4 | $("#campaign_image").val(this.id); 5 | $(this).children("img").addClass("img-selected"); 6 | }); 7 | }); -------------------------------------------------------------------------------- /app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

    Welcome <%= @email %>!

    2 | 3 |

    You can confirm your account email through the link below:

    4 | 5 |

    <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

    6 | -------------------------------------------------------------------------------- /test/controllers/payouts_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PayoutsControllerTest < ActionDispatch::IntegrationTest 4 | test "should require valid payout ID" do 5 | get payout_path('fake') 6 | assert_response :redirect 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def format_amount(amount) 3 | sprintf('$%0.2f', amount.to_f / 100.0).gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,") 4 | end 5 | 6 | def format_date(created) 7 | Time.at(created).getutc.strftime("%m/%d/%Y") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /db/migrate/20161227180421_modify_campaigns_and_charges.rb: -------------------------------------------------------------------------------- 1 | class ModifyCampaignsAndCharges < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :campaigns, :content_link, :string 4 | remove_column :campaigns, :price, :integer 5 | rename_column :charges, :course_id, :campaign_id 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | has_many :campaigns 3 | # Include default devise modules. Others available are: 4 | # :confirmable, :lockable, :timeoutable and :omniauthable 5 | devise :database_authenticatable, :registerable, 6 | :recoverable, :rememberable, :trackable, :validatable 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/charges.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | charge_id: MyString 5 | amount: 1 6 | amount_refunded: 1 7 | campaign_id: 1 8 | 9 | two: 10 | charge_id: MyString 11 | amount: 1 12 | amount_refunded: 1 13 | campaign_id: 1 14 | -------------------------------------------------------------------------------- /app/views/devise/mailer/unlock_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

    Hello <%= @resource.email %>!

    2 | 3 |

    Your account has been locked due to an excessive number of unsuccessful sign in attempts.

    4 | 5 |

    Click the link below to unlock your account:

    6 | 7 |

    <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

    8 | -------------------------------------------------------------------------------- /db/migrate/20160910162519_create_lessons.rb: -------------------------------------------------------------------------------- 1 | class CreateLessons < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :lessons do |t| 4 | t.integer :user_id 5 | t.string :title 6 | t.string :description 7 | t.integer :price 8 | t.boolean :subscription 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/kaminari/_page.html.erb: -------------------------------------------------------------------------------- 1 | <% if page.current? %> 2 |
  • 3 | <%= content_tag :a, page, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)) %> 4 |
  • 5 | <% else %> 6 |
  • 7 | <%= link_to page, url, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)) %> 8 |
  • 9 | <% end %> 10 | -------------------------------------------------------------------------------- /db/migrate/20160913025720_create_charges.rb: -------------------------------------------------------------------------------- 1 | class CreateCharges < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :charges do |t| 4 | t.string :charge_id 5 | t.integer :amount 6 | t.integer :amount_refunded 7 | t.integer :user_id 8 | t.integer :course_id 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/campaign.rb: -------------------------------------------------------------------------------- 1 | class Campaign < ApplicationRecord 2 | belongs_to :user 3 | 4 | validates :title, 5 | presence: true, length: { minimum: 5, maximum: 100 } 6 | 7 | validates :goal, 8 | presence: true, numericality: { greater_than: 20, less_than: 1000000 } 9 | 10 | validates :description, 11 | presence: true, length: { minimum: 10, maximum: 5000 } 12 | end 13 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /app/views/admin/kaminari/_gap.html.erb: -------------------------------------------------------------------------------- 1 | <%# Non-link tag that stands for skipped pages... 2 | - available local variables 3 | current_page: a page object for the currently displayed page 4 | total_pages: total number of pages 5 | per_page: number of items to fetch per page 6 | remote: data-remote 7 | -%> 8 | <%= t('views.pagination.truncate').html_safe %> 9 | -------------------------------------------------------------------------------- /config/initializers/kaminari_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | Kaminari.configure do |config| 3 | config.default_per_page = 12 4 | # config.max_per_page = nil 5 | # config.window = 4 6 | # config.outer_window = 0 7 | # config.left = 0 8 | # config.right = 0 9 | # config.page_method_name = :page 10 | # config.param_name = :page 11 | # config.params_on_first_page = false 12 | end 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore bundler config. 2 | /.bundle 3 | 4 | # Ignore the default SQLite database. 5 | /db/*.sqlite3 6 | /db/*.sqlite3-journal 7 | 8 | # Ignore all logfiles and tempfiles. 9 | /log/* 10 | /tmp/* 11 | !/log/.keep 12 | !/tmp/.keep 13 | 14 | # Ignore Byebug command history file. 15 | .byebug_history 16 | 17 | # Ignore DS_store 18 | .DS_Store 19 | 20 | # Ignore ngrok for local testing 21 | ngrok 22 | -------------------------------------------------------------------------------- /db/migrate/20160911193440_create_stripe_accounts.rb: -------------------------------------------------------------------------------- 1 | class CreateStripeAccounts < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :stripe_accounts do |t| 4 | t.string :first_name 5 | t.string :last_name 6 | t.string :account_type 7 | t.integer :dob_month 8 | t.integer :dob_day 9 | t.integer :dob_year 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/controllers/debit_cards_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DebitCardsControllerTest < ActionDispatch::IntegrationTest 4 | include Devise::Test::IntegrationHelpers 5 | 6 | test "should get new" do 7 | sign_in @user 8 | create_stripe_account 9 | @user.stripe_account = @stripe_account.id 10 | get debit_cards_new_url 11 | assert_response :success 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/stripe_accounts.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | first_name: MyString 5 | last_name: MyString 6 | account_type: MyString 7 | dob_month: 1 8 | dob_day: 1 9 | dob_year: 1 10 | 11 | two: 12 | first_name: MyString 13 | last_name: MyString 14 | account_type: MyString 15 | dob_month: 1 16 | dob_day: 1 17 | dob_year: 1 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the rails generate channel command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /db/migrate/20160911230223_add_fields_to_stripe_account.rb: -------------------------------------------------------------------------------- 1 | class AddFieldsToStripeAccount < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :stripe_accounts, :address_city, :string 4 | add_column :stripe_accounts, :address_state, :string 5 | add_column :stripe_accounts, :address_line1, :string 6 | add_column :stripe_accounts, :address_postal, :string 7 | add_column :stripe_accounts, :tos, :boolean 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/stripe_accounts/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Submit your info' %> 2 |
    3 |
    4 |
    5 |

    Submit your account application

    6 |
    7 | <%= render 'layouts/messages' %> 8 | <%= render 'account_form' %> 9 |
    10 |
    11 |
    12 |
    13 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

    Hello <%= @resource.email %>!

    2 | 3 |

    Someone has requested a link to change your password. You can do this through the link below.

    4 | 5 |

    <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

    6 | 7 |

    If you didn't request this, please ignore this email.

    8 |

    Your password won't change until you access the link above and create a new one.

    9 | -------------------------------------------------------------------------------- /db/migrate/20170405165023_add_more_fields_to_stripe_accounts.rb: -------------------------------------------------------------------------------- 1 | class AddMoreFieldsToStripeAccounts < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :stripe_accounts, :ssn_last_4, :string 4 | add_column :stripe_accounts, :business_name, :string 5 | add_column :stripe_accounts, :business_tax_id, :string 6 | add_column :stripe_accounts, :personal_id_number, :string 7 | add_column :stripe_accounts, :verification_document, :string 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found 4 | 5 | # Pretty generic method to handle exceptions. 6 | # You'll probably want to do some logging, notifications, etc. 7 | def handle_error(message = "Sorry, something failed.", view = 'new') 8 | flash.now[:alert] = message 9 | render view 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/helpers/devise_helper.rb: -------------------------------------------------------------------------------- 1 | module DeviseHelper 2 | def devise_error_messages! 3 | return "" if resource.errors.empty? 4 | messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join 5 | html = <<-HTML 6 |
    7 | × 8 | 9 |
    10 | HTML 11 | html.html_safe 12 | end 13 | end -------------------------------------------------------------------------------- /app/views/admin/kaminari/_last_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "Last" page 2 | - available local variables 3 | url: url to the last page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote 8 | -%> 9 | 10 | <%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, remote: remote %> 11 | 12 | -------------------------------------------------------------------------------- /app/views/admin/kaminari/_first_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "First" page 2 | - available local variables 3 | url: url to the first page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote 8 | -%> 9 | 10 | <%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %> 11 | 12 | -------------------------------------------------------------------------------- /app/views/layouts/_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |key, value| %> 2 |
    3 | × 4 | 17 |
    18 | <% end %> -------------------------------------------------------------------------------- /app/models/stripe_account.rb: -------------------------------------------------------------------------------- 1 | class StripeAccount < ApplicationRecord 2 | validates :first_name, 3 | presence: true, length: { minimum: 1, maximum: 40 } 4 | 5 | validates :last_name, 6 | presence: true, length: { minimum: 1, maximum: 40 } 7 | 8 | validates :account_type, 9 | presence: true, inclusion: { in: %w(individual company), message: "%{value} is not a valid account type"} 10 | 11 | validates :tos, 12 | inclusion: { in: [ true ], message: ": You must agree to the terms of service" } 13 | end 14 | -------------------------------------------------------------------------------- /app/views/admin/kaminari/_next_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "Next" page 2 | - available local variables 3 | url: url to the next page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote 8 | -%> 9 | 10 | <%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote %> 11 | 12 | -------------------------------------------------------------------------------- /app/views/devise/unlocks/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Resend unlock instructions

    2 | 3 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |
    7 | <%= f.label :email %>
    8 | <%= f.email_field :email, autofocus: true %> 9 |
    10 | 11 |
    12 | <%= f.submit "Resend unlock instructions" %> 13 |
    14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /app/views/admin/kaminari/_prev_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "Previous" page 2 | - available local variables 3 | url: url to the previous page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote 8 | -%> 9 | 10 | <%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote %> 11 | 12 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) 11 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } 12 | gem 'spring', match[1] 13 | require 'spring/binstub' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/admin/kaminari/_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link showing page number 2 | - available local variables 3 | page: a page object for "this" page 4 | url: url to this page 5 | current_page: a page object for the currently displayed page 6 | total_pages: total number of pages 7 | per_page: number of items to fetch per page 8 | remote: data-remote 9 | -%> 10 | 11 | <%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %> 12 | 13 | -------------------------------------------------------------------------------- /app/views/campaigns/_verification.html.erb: -------------------------------------------------------------------------------- 1 | <% if @stripe_account.requirements.currently_due %> 2 |
    3 |
    4 |
    5 |

    6 | We need some information to continue processing donations for you. 7 |

    8 |

    9 | <%= link_to "Update your information", edit_stripe_account_path(@stripe_account.id), class: "btn btn-lg btn-warning btn-custom" %> 10 |

    11 |
    12 |
    13 |
    14 | <% end %> 15 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module SellerDashboard 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: { 8 | id: 1, 9 | email: "email@hotmail.com", 10 | encrypted_password: "$2a$10$ZU0x7nDXA1EKbUAjwKTCwegV/yfddZkvfKWEa9/arb5pBQuy/1oFu" 11 | } 12 | # column: value 13 | # 14 | two: { 15 | email: user2@test.com 16 | } 17 | # column: value 18 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.erb: -------------------------------------------------------------------------------- 1 | <%= paginator.render do -%> 2 | 15 | <% end -%> 16 | -------------------------------------------------------------------------------- /app/views/campaigns/_donations.html.erb: -------------------------------------------------------------------------------- 1 | <% @charges.each do | charge | %> 2 |
    3 |
    4 |

    5 | 6 | 7 | 8 | 9 | <%= number_to_currency(charge.amount/100) %> donated by <%= charge.name.split.first %> 10 | <%= time_ago_in_words(charge.created_at) %> ago 11 |

    12 |
    13 |
    14 |
    15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/views/devise/confirmations/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Resend confirmation instructions

    2 | 3 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |
    7 | <%= f.label :email %>
    8 | <%= f.email_field :email, autofocus: true, value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> 9 |
    10 | 11 |
    12 | <%= f.submit "Resend confirmation instructions" %> 13 |
    14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /app/views/layouts/_footer.html.erb: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /app/views/campaigns/_transactions.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <% @transactions.each do |date,net| %> 12 | <% unless net.eql?(0) %> 13 | 14 | 15 | 16 | 17 | <% end %> 18 | <% end %> 19 | 20 |
    Balance amountAvailable for transfer after
    <%= format_amount(net) %><%= format_date(date) %>
    21 |
    22 |
    -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= yield(:page_title) %> 8 | <%= csrf_meta_tags %> 9 | <%= stylesheet_link_tag 'application', media: 'all' %> 10 | <%= javascript_include_tag 'application' %> 11 | <%= favicon_link_tag 'favicon.ico' %> 12 | <%= yield(:header) %> 13 | 14 | 15 | 16 | <%= render 'layouts/header' %> 17 | <%= yield %> 18 | <%= render 'layouts/footer' %> 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/views/stripe_accounts/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Submit your account info' %> 2 |
    3 |
    4 |
    5 |

    6 | Update your account 7 |

    8 |
    9 | <%= render 'layouts/messages' %> 10 | <%= render 'identity_verification_form' %> 11 |
    12 |
    13 |
    14 |
    15 | 18 |
    19 |
    -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /app/views/pages/terms.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Terms' %> 2 |
    3 |
    4 |
    5 |

    Terms of service

    6 |
    7 |
    8 |
    9 |
    10 |

    11 | Your managed accounts must agree to Stripe's Connected Account Agreement. Read more about this in this support article. 12 |

    13 |
    14 |
    15 |
    -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require font-awesome 14 | *= require_self 15 | *= require_tree . 16 | */ 17 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | devise_for :users, controllers: { registrations: "registrations" } 4 | 5 | root to: "campaigns#home" 6 | get 'dashboard', to: 'campaigns#dashboard' 7 | get 'pricing', to: 'pages#pricing' 8 | get 'terms', to: 'pages#terms' 9 | get 'stripe_accounts/full', to: 'stripe_accounts#full' 10 | get 'debit_cards/new' 11 | post 'debit_cards/create', to: 'debit_cards#create' 12 | post 'debit_cards/destroy', to: 'debit_cards#destroy' 13 | post 'disputes', to: 'disputes#create' 14 | post 'instant_transfer', to: 'debit_cards#transfer' 15 | get 'payouts/:id', to: 'payouts#show', as: 'payout' 16 | post 'webhooks/stripe', to: 'webhooks#stripe' 17 | 18 | resources :campaigns 19 | 20 | resources :stripe_accounts 21 | 22 | resources :charges 23 | 24 | resources :bank_accounts 25 | end 26 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require bootstrap 15 | //= require jquery_ujs 16 | //= require data-confirm-modal 17 | //= require_tree . -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /app/views/devise/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |

    Change your password

    2 | 3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> 4 | <%= devise_error_messages! %> 5 | <%= f.hidden_field :reset_password_token %> 6 | 7 |
    8 | <%= f.label :password, "New password" %>
    9 | <% if @minimum_password_length %> 10 | (<%= @minimum_password_length %> characters minimum)
    11 | <% end %> 12 | <%= f.password_field :password, autofocus: true, autocomplete: "off" %> 13 |
    14 | 15 |
    16 | <%= f.label :password_confirmation, "Confirm new password" %>
    17 | <%= f.password_field :password_confirmation, autocomplete: "off" %> 18 |
    19 | 20 |
    21 | <%= f.submit "Change my password" %> 22 |
    23 | <% end %> 24 | 25 | <%= render "devise/shared/links" %> 26 | -------------------------------------------------------------------------------- /test/controllers/charges_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ChargesControllerTest < ActionDispatch::IntegrationTest 4 | include Devise::Test::IntegrationHelpers 5 | 6 | test "should not show charges unless logged in" do 7 | get charge_path(@campaign) 8 | assert_redirected_to new_user_session_path 9 | end 10 | 11 | test "should require token to create a charge" do 12 | post charges_path, params: { amount: 100, name: "User", campaign: @campaign } 13 | assert_response :redirect 14 | end 15 | 16 | test "should create successful charge" do 17 | create_stripe_account 18 | @campaign.user_id = @user.id 19 | @user.stripe_account = @stripe_account.id 20 | @user.save 21 | charge_amount = rand(10..50) 22 | post charges_path, params: { amount: charge_amount, name: "User", campaign: @campaign.id, stripeToken: 'tok_visa' } 23 | assert_equal(charge_amount*100, Charge.last.amount) 24 | end 25 | 26 | 27 | end 28 | -------------------------------------------------------------------------------- /app/views/admin/kaminari/_paginator.html.erb: -------------------------------------------------------------------------------- 1 | <%# The container tag 2 | - available local variables 3 | current_page: a page object for the currently displayed page 4 | total_pages: total number of pages 5 | per_page: number of items to fetch per page 6 | remote: data-remote 7 | paginator: the paginator that renders the pagination tags inside 8 | -%> 9 | <%= paginator.render do -%> 10 | 25 | <% end -%> 26 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: cb8b29ab66181a6dbefabc9939b0c9e599a8788fd450a218caad32bac552aaa473836aa7286019b7d108fc4a968b7363f8814fa35b14efe4dd3fe85ee8f76421 15 | 16 | test: 17 | secret_key_base: 85767ecd3e5e4b26f92be965ec4bc5cc2850a13ef4999bb208039b68b80e020669ce791e3f91e048b80ad3693a5e45f61c77e61740eca164ccf32962882784b2 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /app/views/pages/pricing.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Pricing' %> 2 |
    3 |
    4 |
    5 |

    Simple, transparent pricing

    6 |
    7 |

    10%

    8 | Fee on funds raised 9 |
    10 |
    11 |
    12 |

    $0

    13 | Refunds 14 |
    15 |
    16 |

    $15

    17 | Dispute fee 18 |
    19 |
    20 |

    $0

    21 | Setup 22 |
    23 |
    24 |
    25 |

    26 | <%= link_to "Sign up for an account", new_user_registration_path, class: "btn btn-xl btn-primary btn-custom btn-block shadow-sm" %> 27 |

    28 |
    29 |
    30 |
    31 | -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Reset your password' %> 2 |
    3 |
    4 |
    5 |

    Forgot your password?

    6 | <%= devise_error_messages! %> 7 | <%= render 'layouts/messages' %> 8 |
    9 |
    10 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> 11 |
    12 | <%= f.label :email %> 13 | <%= f.email_field :email, autofocus: true, class: "form-control input-lg", placeholder: "Email address" %> 14 |
    15 |
    16 | <%= f.button "Reset password", class: "btn btn-lg btn-primary btn-block btn-custom", data: {disable_with: " Resetting..."} %> 17 |
    18 | <% end %> 19 |
    20 |
    21 |
    22 |
    23 |
    -------------------------------------------------------------------------------- /test/controllers/campaigns_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CampaignsControllerTest < ActionDispatch::IntegrationTest 4 | include Devise::Test::IntegrationHelpers 5 | 6 | test "should load root path" do 7 | get root_path 8 | assert_response :success 9 | assert_select 'title', "Stripe Connect Example App" 10 | end 11 | 12 | test "should load campaign" do 13 | get campaign_path(@campaign) 14 | assert_response :success 15 | end 16 | 17 | test "should require login to create a campaign" do 18 | get new_campaign_path 19 | assert_redirected_to new_user_session_path 20 | end 21 | 22 | test "should redirect if no stripe account" do 23 | sign_in @user 24 | get new_campaign_path 25 | assert_redirected_to new_stripe_account_path 26 | end 27 | 28 | test 'should create a campaign successfully' do 29 | sign_in @user 30 | create_stripe_account 31 | @user.stripe_account = @stripe_account.id 32 | post campaigns_path, params: { campaign: { title: "Title", description: "Help me do a thing", goal: 100, image: "https://unsplash.it" } } 33 | assert_redirected_to campaign_path(Campaign.last) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Rails 5.0 release notes for more info on each option. 6 | 7 | # Enable per-form CSRF tokens. Previous versions had false. 8 | Rails.application.config.action_controller.per_form_csrf_tokens = true 9 | 10 | # Enable origin-checking CSRF mitigation. Previous versions had false. 11 | Rails.application.config.action_controller.forgery_protection_origin_check = true 12 | 13 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 14 | # Previous versions had false. 15 | ActiveSupport.to_time_preserves_timezone = true 16 | 17 | # Require `belongs_to` associations by default. Previous versions had false. 18 | Rails.application.config.active_record.belongs_to_required_by_default = true 19 | 20 | # Do not halt callback chains when a callback returns false. Previous versions had true. 21 | ActiveSupport.halt_callback_chains_on_return_false = false 22 | 23 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 24 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 25 | -------------------------------------------------------------------------------- /test/controllers/bank_accounts_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BankAccountsControllerTest < ActionDispatch::IntegrationTest 4 | include Devise::Test::IntegrationHelpers 5 | 6 | test "should redirect if not authenticated" do 7 | get new_bank_account_path 8 | assert_redirected_to new_user_session_path 9 | end 10 | 11 | test 'should not allow post if not authenticated' do 12 | post bank_accounts_path "stripeToken": "tok_123" 13 | assert_redirected_to new_user_session_path 14 | end 15 | 16 | test 'should redirect if no stripe account' do 17 | sign_in @user 18 | get new_bank_account_path 19 | assert_redirected_to new_stripe_account_path 20 | end 21 | 22 | test 'should load bank accounts page if stripe account exists' do 23 | sign_in @user 24 | @user.stripe_account = "acct_abc123" 25 | get new_bank_account_path 26 | assert_response :success 27 | end 28 | 29 | test 'should attach a valid bank account' do 30 | sign_in @user 31 | create_stripe_account 32 | @user.stripe_account = @stripe_account.id 33 | create_bank_token 34 | post bank_accounts_path, params: { stripeToken: @btok.id } 35 | assert_redirected_to dashboard_path 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/views/devise/shared/_links.html.erb: -------------------------------------------------------------------------------- 1 | <%- if controller_name != 'sessions' %> 2 | <%= link_to "Log in", new_session_path(resource_name) %>
    3 | <% end -%> 4 | 5 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %> 6 | <%= link_to "Sign up", new_registration_path(resource_name) %>
    7 | <% end -%> 8 | 9 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> 10 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
    11 | <% end -%> 12 | 13 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> 14 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
    15 | <% end -%> 16 | 17 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> 18 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
    19 | <% end -%> 20 | 21 | <%- if devise_mapping.omniauthable? %> 22 | <%- resource_class.omniauth_providers.each do |provider| %> 23 | <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
    24 | <% end -%> 25 | <% end -%> 26 | -------------------------------------------------------------------------------- /app/views/debit_cards/_debit_card_form.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | <%= hidden_field_tag :authenticity_token, form_authenticity_token %> 5 |
    6 |
    7 |
    8 |
    9 | 10 | 11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 | 18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 | 28 |
    29 |
    30 |
    31 |
    32 |
    -------------------------------------------------------------------------------- /app/views/campaigns/_checkoutform.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | <%= hidden_field_tag :authenticity_token, form_authenticity_token %> 5 |
    6 |
    7 |
    8 | 9 | 10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 | 17 | 18 |
    19 |
    20 |
    21 | 22 | 23 | 24 |
    25 |
    26 |
    27 |

    28 | Use Stripe Elements 29 |

    30 | -------------------------------------------------------------------------------- /app/views/campaigns/_campaigns.html.erb: -------------------------------------------------------------------------------- 1 | <% @campaigns.each_slice(3) do | campaigns | %> 2 |
    3 | <% campaigns.each do |campaign| %> 4 |
    5 |
    6 | <%= link_to campaign_path(campaign) do %> 7 |
    8 | <%= image_tag("#{campaign.image}_sm.jpg") %> 9 |
    10 |

    <%= link_to campaign.title.titleize, campaign_path(campaign) %>

    11 |

    12 | <% if campaign.raised %> 13 | <%= number_to_currency(campaign.raised/100, precision: 0) %> raised so far of <%= number_to_currency(campaign.goal, precision: 0) %> goal 14 | <% else %> 15 | <%= number_to_currency(campaign.goal, precision: 0) %> goal 16 | <% end %> 17 |

    18 |

    19 | <%= simple_format(truncate(campaign.description, length: 90)) %> 20 |

    21 |
    22 |
    23 | <% end %> 24 |
    25 |
    26 | <% end %> 27 |
    28 | <% end %> 29 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | Stripe.api_key = ENV['SECRET_KEY'] 6 | 7 | class ActiveSupport::TestCase 8 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 9 | fixtures :all 10 | 11 | setup do 12 | @user = users(:one) 13 | @campaign = campaigns(:one) 14 | end 15 | 16 | # Create a US bank token 17 | def create_bank_token 18 | @btok = Stripe::Token.create( 19 | bank_account: { 20 | country: "US", 21 | currency: "usd", 22 | routing_number: "110000000", 23 | account_number: "000123456789" 24 | } 25 | ) 26 | end 27 | 28 | # Create a Stripe account to use for tests 29 | def create_stripe_account 30 | @stripe_account = Stripe::Account.create( 31 | type: 'custom', 32 | requested_capabilities: ['platform_payments'], # Donors interact with the platform 33 | business_type: 'company', 34 | company: { 35 | name: 'Snookies Cookies', 36 | tax_id: '000000000', 37 | }, 38 | business_profile: { 39 | product_description: 'Fundraising campaign', 40 | }, 41 | tos_acceptance: { 42 | date: Time.now.to_i, 43 | ip: '1.2.3.4' 44 | }, 45 | ) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/views/layouts/_header.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/campaigns/_transfers.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <% @payouts.each do |payout| %> 14 | 15 | 24 | 31 | 32 | 33 | 34 | <% end %> 35 | 36 |
    Transfer IDDestinationAmountDeposit date
    16 | <% if payout.type.eql?('bank_account') %> 17 | 18 | <%= link_to payout.id, payout_path(payout.id), class: "leftspace" %> 19 | <% else %> 20 | 21 | <%= payout.id %> 22 | <% end %> 23 | 25 | <% if payout.type.eql?('bank_account') %> 26 | <%= payout.destination.bank_name %> ending in <%= payout.destination.last4 %> 27 | <% elsif payout.type.eql?('card') %> 28 | <%= payout.destination.brand %> ending in <%= payout.destination.last4 %> 29 | <% end %> 30 | <%= format_amount(payout.amount) %><%= format_date(payout.arrival_date) %>
    37 |
    38 |
    39 | -------------------------------------------------------------------------------- /app/controllers/payouts_controller.rb: -------------------------------------------------------------------------------- 1 | class PayoutsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def show 5 | # Retrieve the payout from Stripe to get details 6 | # For large production applications, it's usually best to store this state locally 7 | if params[:id] 8 | begin 9 | @stripe_account = Stripe::Account.retrieve(current_user.stripe_account) 10 | 11 | # Get the payout details 12 | @payout = Stripe::Payout.retrieve( 13 | { 14 | id: params[:id] 15 | }, 16 | { stripe_account: current_user.stripe_account } 17 | ) 18 | 19 | # Get the balance transactions from the payout 20 | @txns = Stripe::BalanceTransaction.list( 21 | { 22 | payout: params[:id], 23 | expand: ['data.source.source_transfer', 'data.source.charge.source_transfer'], 24 | limit: 100 25 | }, 26 | { stripe_account: current_user.stripe_account } 27 | ) 28 | # Handle exceptions from Stripe 29 | rescue Stripe::StripeError => e 30 | flash[:error] = e.message 31 | redirect_to dashboard_path 32 | rescue => e 33 | # Something else happened, completely unrelated to Stripe 34 | flash[:error] = e.message 35 | redirect_to dashboard_path 36 | end 37 | else 38 | flash[:error] = "Sorry, this payout was not found" 39 | redirect_to dashboard_path 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/controllers/disputes_controller.rb: -------------------------------------------------------------------------------- 1 | class DisputesController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def create 5 | if dispute_params[:dispute_text].empty? || dispute_params[:dispute_id].empty? 6 | flash[:error] = "Please provide supporting information about this dispute" 7 | redirect_back(fallback_location: root_path) and return 8 | end 9 | 10 | begin 11 | # Retrieve the account object for this user 12 | account = Stripe::Account.retrieve(current_user.stripe_account) 13 | 14 | # Retrieve the dispute 15 | dispute = Stripe::Dispute.retrieve(dispute_params[:dispute_id]) 16 | 17 | # Add the dispute evidence 18 | dispute.evidence.uncategorized_text = dispute_params[:dispute_text] 19 | # Add dispute document if one exists 20 | dispute.evidence.uncategorized_file = dispute_params[:dispute_document] 21 | dispute.save 22 | 23 | # Success, send back to the page 24 | flash[:success] = "This dispute has been updated" 25 | redirect_back(fallback_location: root_path) and return 26 | 27 | # Handle exceptions from Stripe 28 | rescue Stripe::StripeError => e 29 | flash[:error] = e.message 30 | redirect_to dashboard_path 31 | 32 | # Handle any other exceptions 33 | rescue => e 34 | flash[:error] = e.message 35 | redirect_to dashboard_path 36 | end 37 | end 38 | 39 | private 40 | def dispute_params 41 | params.permit(:dispute_id, :dispute_text, :dispute_document) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /db/migrate/20160910151553_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :users do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.string :current_sign_in_ip 20 | t.string :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps null: false 35 | end 36 | 37 | add_index :users, :email, unique: true 38 | add_index :users, :reset_password_token, unique: true 39 | # add_index :users, :confirmation_token, unique: true 40 | # add_index :users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Sign in' %> 2 |
    3 |
    4 |
    5 |

    Sign in to your account

    6 | <%= devise_error_messages! %> 7 | <%= render 'layouts/messages' %> 8 |
    9 |
    10 | <%= form_for(resource, as: resource_name, 11 | url: session_path(resource_name)) do |f| %> 12 |
    13 | <%= f.label :email %> 14 | <%= f.email_field :email, autofocus: true, class: "form-control input-lg", placeholder: "Email address" %> 15 |
    16 |
    17 | <%= f.label :password %> 18 | <%= f.password_field :password, autocomplete: "off", class: "form-control input-lg", placeholder: "Password" %> 19 |
    20 |
    21 | <%= f.button "Log in", class: "btn btn-lg btn-block btn-primary btn-custom", data: {disable_with: " Logging in..."} %> 22 |
    23 | <% end %> 24 |
    25 | <%= link_to "Forgot your password?", new_user_password_path %> 26 |
    27 |
    28 |
    29 |
    30 | Need an account? <%= link_to "Sign up", new_user_registration_path %> 31 |
    32 |
    33 |
    34 |
    -------------------------------------------------------------------------------- /app/controllers/bank_accounts_controller.rb: -------------------------------------------------------------------------------- 1 | class BankAccountsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def new 5 | # Redirect if no stripe account exists yet 6 | unless current_user.stripe_account 7 | redirect_to new_stripe_account_path and return 8 | end 9 | 10 | begin 11 | # Retrieve the account object for this user 12 | @account = Stripe::Account.retrieve(current_user.stripe_account) 13 | 14 | # Handle exceptions from Stripe 15 | rescue Stripe::StripeError => e 16 | handle_error(e.message, 'new') 17 | 18 | # Handle any other exceptions 19 | rescue => e 20 | handle_error(e.message, 'new') 21 | end 22 | end 23 | 24 | def create 25 | # Redirect if no token is POSTed or the user doesn't have a Stripe account 26 | unless params[:stripeToken] && current_user.stripe_account 27 | redirect_to new_bank_account_path and return 28 | end 29 | 30 | begin 31 | # Retrieve the account object for this user 32 | account = Stripe::Account.retrieve(current_user.stripe_account) 33 | 34 | # Create the bank account 35 | account.external_account = params[:stripeToken] 36 | account.save 37 | 38 | # Success, send on to the dashboard 39 | flash[:success] = "Your bank account has been added!" 40 | redirect_to dashboard_path 41 | 42 | # Handle exceptions from Stripe 43 | rescue Stripe::StripeError => e 44 | handle_error(e.message, 'new') 45 | 46 | # Handle any other exceptions 47 | rescue => e 48 | handle_error(e.message, 'new') 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/controllers/stripe_accounts_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StripeAccountsControllerTest < ActionDispatch::IntegrationTest 4 | include Devise::Test::IntegrationHelpers 5 | 6 | test "should redirect if not logged in" do 7 | get new_stripe_account_path 8 | assert_redirected_to new_user_session_path 9 | end 10 | 11 | test "should require existing stripe account to edit" do 12 | sign_in @user 13 | get edit_stripe_account_path("acct_123") 14 | assert_redirected_to dashboard_path 15 | end 16 | 17 | test "should load new account creation path" do 18 | sign_in @user 19 | get new_stripe_account_path 20 | assert_response :success 21 | end 22 | 23 | test "should reject invalid account details and throw an error" do 24 | sign_in @user 25 | post stripe_accounts_path, params: { 26 | stripe_account: { 27 | dob_day: "100", 28 | dob_month: "00", 29 | dob_year: "1992", 30 | account_type: "individual", 31 | tos: "true" 32 | } 33 | } 34 | assert_nil @user.stripe_account 35 | assert_not_empty flash 36 | end 37 | 38 | test "should successfully create an account" do 39 | sign_in @user 40 | post stripe_accounts_path, params: { 41 | stripe_account: { 42 | first_name: "Test", 43 | last_name: "Mctesterson", 44 | dob_day: "29", 45 | dob_month: "04", 46 | dob_year: "1992", 47 | account_type: "individual", 48 | tos: "true" 49 | } 50 | } 51 | @user.reload 52 | assert_match(/acct_/, @user.stripe_account) 53 | assert_redirected_to new_bank_account_path 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /app/helpers/stripe_accounts_helper.rb: -------------------------------------------------------------------------------- 1 | module StripeAccountsHelper 2 | def states 3 | [ 4 | ['Alabama', 'AL'], 5 | ['Alaska', 'AK'], 6 | ['Arizona', 'AZ'], 7 | ['Arkansas', 'AR'], 8 | ['California', 'CA'], 9 | ['Colorado', 'CO'], 10 | ['Connecticut', 'CT'], 11 | ['Delaware', 'DE'], 12 | ['District of Columbia', 'DC'], 13 | ['Florida', 'FL'], 14 | ['Georgia', 'GA'], 15 | ['Hawaii', 'HI'], 16 | ['Idaho', 'ID'], 17 | ['Illinois', 'IL'], 18 | ['Indiana', 'IN'], 19 | ['Iowa', 'IA'], 20 | ['Kansas', 'KS'], 21 | ['Kentucky', 'KY'], 22 | ['Louisiana', 'LA'], 23 | ['Maine', 'ME'], 24 | ['Maryland', 'MD'], 25 | ['Massachusetts', 'MA'], 26 | ['Michigan', 'MI'], 27 | ['Minnesota', 'MN'], 28 | ['Mississippi', 'MS'], 29 | ['Missouri', 'MO'], 30 | ['Montana', 'MT'], 31 | ['Nebraska', 'NE'], 32 | ['Nevada', 'NV'], 33 | ['New Hampshire', 'NH'], 34 | ['New Jersey', 'NJ'], 35 | ['New Mexico', 'NM'], 36 | ['New York', 'NY'], 37 | ['North Carolina', 'NC'], 38 | ['North Dakota', 'ND'], 39 | ['Ohio', 'OH'], 40 | ['Oklahoma', 'OK'], 41 | ['Oregon', 'OR'], 42 | ['Pennsylvania', 'PA'], 43 | ['Puerto Rico', 'PR'], 44 | ['Rhode Island', 'RI'], 45 | ['South Carolina', 'SC'], 46 | ['South Dakota', 'SD'], 47 | ['Tennessee', 'TN'], 48 | ['Texas', 'TX'], 49 | ['Utah', 'UT'], 50 | ['Vermont', 'VT'], 51 | ['Virginia', 'VA'], 52 | ['Washington', 'WA'], 53 | ['West Virginia', 'WV'], 54 | ['Wisconsin', 'WI'], 55 | ['Wyoming', 'WY'] 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/views/campaigns/_paymentform.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | <%= hidden_field_tag :authenticity_token, form_authenticity_token %> 5 |
    6 |
    7 |
    8 | 9 | 10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 | 17 | 18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 | 33 | 36 |
    37 |
    38 |
    39 |
    40 |
    41 |

    42 | Use Stripe Checkout 43 |

    44 | -------------------------------------------------------------------------------- /app/views/devise/registrations/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Sign up' %> 2 |
    3 |
    4 |
    5 |

    Sign up

    6 | <%= devise_error_messages! %> 7 | <%= render 'layouts/messages' %> 8 |
    9 |
    10 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> 11 |
    12 | <%= f.label :email %> 13 | <%= f.email_field :email, class: 'form-control input-lg', placeholder: "Email", autofocus: true %> 14 |
    15 |
    16 | <%= f.label :password %> 17 | <%= f.password_field :password, autocomplete: "off", class: 'form-control input-lg', placeholder: "Password" %> 18 |
    19 |
    20 | <%= f.label :password_confirmation %> 21 | <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control input-lg', placeholder: "Confirm password" %> 22 |
    23 |
    24 | <%= f.button "Sign up", class: "btn btn-primary btn-lg btn-block btn-custom", data: {disable_with: " Signing up..."} %> 25 |
    26 | <% end %> 27 |
    28 |
    29 |
    30 | Already have an account? <%= link_to "Sign in", new_user_session_path %> 31 |
    32 |
    33 |
    34 |
    -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    We're sorry, but something went wrong.

    62 |
    63 |

    If you are the application owner check the logs for more information.

    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /app/views/campaigns/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Edit your campaign' %> 2 |
    3 |
    4 |
    5 |

    Edit your campaign

    6 |
    7 | <%= render 'layouts/messages' %> 8 |
    9 |
    10 | <%= form_for @campaign do | f | %> 11 |
    12 | <%= f.label :title, "Title" %> 13 | <%= f.text_field :title, class: "form-control input-lg" %> 14 |
    15 |
    16 | <%= f.label :goal, "Goal" %> 17 |
    18 | $ 19 | <%= f.text_field :goal, class: "form-control input-lg" %> 20 |
    21 |
    22 |
    23 | <%= f.label :description, "Description" %> 24 | <%= f.text_area :description, class: "form-control input-lg" %> 25 |
    26 |
    27 | <%= render 'images' %> 28 |
    29 |
    30 | <%= f.button "Update Campaign", class: "btn btn-primary btn-lg btn-block btn-custom", data: {disable_with: " Updating campaign..."} %> 31 |
    32 | <%= f.hidden_field :image %> 33 | <% end %> 34 |
    35 |
    36 |
    37 |
    38 |
    39 |
    -------------------------------------------------------------------------------- /app/views/campaigns/home.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Stripe Connect Example App' %> 2 | <% if params[:page] %> 3 | 4 | <% else %> 5 |
    6 |
    7 |
    8 |
    9 | <%= render 'layouts/messages' %> 10 |

    Fundraising Marketplace

    11 |

    A Stripe Connect example app

    12 |

    13 | <% if user_signed_in? %> 14 | <%= link_to "Create a campaign", new_campaign_path, class: "btn btn-xl btn-primary btn-custom btn-block shadow-sm" %> 15 | <% else %> 16 | <%= link_to "Create an account", new_user_registration_path, class: "btn btn-xl btn-primary btn-custom btn-block shadow-sm" %> 17 | <% end %> 18 |

    19 |
    20 |
    21 |
    22 |
    23 | <% end %> 24 |
    25 |
    26 |
    27 | 28 | <%= render 'campaigns' %> 29 |
    30 |
    31 |

    32 | <%= page_entries_info @campaigns, entry_name: 'campaign' %> 33 |

    34 |

    35 | <%= link_to_previous_page @campaigns, 'Previous Page', class: "btn btn-primary btn-custom" %> 36 | <%= link_to_next_page @campaigns, 'Next Page', class: "btn btn-primary btn-custom" %> 37 |

    38 |
    39 |
    40 |
    41 |
    42 |
    43 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

    Maybe you tried to change something you didn't have access to.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

    You may have mistyped the address or the page may have moved.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /app/views/bank_accounts/_bank_account_form.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 |
    7 |
    8 | 9 | 12 |
    13 |
    14 |
    15 |
    16 | 17 | 20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 | 27 | 28 |
    29 |
    30 |
    31 |
    32 | 33 | 34 |
    35 |
    36 |
    37 |
    38 |
    39 | 40 |
    41 |
    42 | <%= hidden_field_tag :authenticity_token, form_authenticity_token -%> 43 |
    44 |
    45 |
    -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /app/views/campaigns/_images.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | <%= image_tag("money_sm.jpg", alt: "Generic money image", class: "img-thumbnail") %> 5 | 6 |
    7 |
    8 | 9 | <%= image_tag("travel_sm.jpg", alt: "Generic travel image", class: "img-thumbnail") %> 10 | 11 |
    12 |
    13 | 14 | <%= image_tag("beach_sm.jpg", alt: "Generic beach image", class: "img-thumbnail") %> 15 | 16 |
    17 |
    18 |
    19 |
    20 | 21 | <%= image_tag("desk_sm.jpg", alt: "Generic desk image", class: "img-thumbnail") %> 22 | 23 |
    24 |
    25 | 26 | <%= image_tag("puppy_sm.jpg", alt: "Generic puppy image", class: "img-thumbnail") %> 27 | 28 |
    29 |
    30 | 31 | <%= image_tag("snow_sm.jpg", alt: "Generic snow image", class: "img-thumbnail") %> 32 | 33 |
    34 |
    35 |
    36 |
    37 | 38 | <%= image_tag("ocean_sm.jpg", alt: "Generic ocean image", class: "img-thumbnail") %> 39 | 40 |
    41 |
    42 | 43 | <%= image_tag("sf_sm.jpg", alt: "Generic SF image", class: "img-thumbnail") %> 44 | 45 |
    46 |
    47 | 48 | <%= image_tag("wedding_sm.jpg", alt: "Generic wedding image", class: "img-thumbnail") %> 49 | 50 |
    51 |
    -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | # Suppress logger output for asset requests. 46 | config.assets.quiet = true 47 | 48 | # Raises error for missing translations 49 | # config.action_view.raise_on_missing_translations = true 50 | 51 | # Use an evented file watcher to asynchronously detect changes in source code, 52 | # routes, locales, etc. This feature depends on the listen gem. 53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 54 | 55 | # Configure actionmailer for devise 56 | config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 57 | end 58 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 5 | gem 'rails', '~> 5.0.0', '>= 5.0.0.1' 6 | # Use Puma as the app server 7 | gem 'puma', '~> 3.0' 8 | # Use SCSS for stylesheets 9 | gem 'sass-rails', '~> 5.0' 10 | # Use Uglifier as compressor for JavaScript assets 11 | gem 'uglifier', '>= 1.3.0' 12 | # Use CoffeeScript for .coffee assets and views 13 | gem 'coffee-rails', '~> 4.2' 14 | # See https://github.com/rails/execjs#readme for more supported runtimes 15 | # gem 'therubyracer', platforms: :ruby 16 | 17 | # Use jquery as the JavaScript library 18 | gem 'jquery-rails' 19 | 20 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 21 | gem 'jbuilder', '~> 2.5' 22 | # Use Redis adapter to run Action Cable in production 23 | # gem 'redis', '~> 3.0' 24 | # Use ActiveModel has_secure_password 25 | # gem 'bcrypt', '~> 3.1.7' 26 | 27 | # Use Capistrano for deployment 28 | # gem 'capistrano-rails', group: :development 29 | 30 | # Stripe for payments... oh hell yes 31 | gem 'stripe' 32 | # Bootstrap for the pretty 33 | gem 'bootstrap-sass', '~> 3.3.6' 34 | # Use Font Awesome to make pretty icons 35 | gem 'font-awesome-rails' 36 | # Devise for use auth 37 | gem 'devise', '~> 4.2', '>= 4.2.1' 38 | # Confirmable modals 39 | gem 'data-confirm-modal' 40 | # Mailgun to gun the mails 41 | gem 'mailgun-ruby' 42 | # Paginate all the things 43 | gem 'kaminari' 44 | 45 | group :development, :test do 46 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 47 | gem 'byebug', platform: :mri 48 | # Use sqlite3 as the database for Active Record 49 | gem 'sqlite3' 50 | end 51 | 52 | group :development do 53 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 54 | gem 'web-console' 55 | gem 'listen', '~> 3.0.5' 56 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 57 | gem 'spring' 58 | gem 'spring-watcher-listen', '~> 2.0.0' 59 | end 60 | 61 | group :production do 62 | gem 'pg', '~> 0.18.4' 63 | end 64 | 65 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 66 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 67 | -------------------------------------------------------------------------------- /app/views/campaigns/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Sell a course' %> 2 |
    3 |
    4 |
    5 |

    Create a campaign

    6 |
    7 | <%= render 'layouts/messages' %> 8 |
    9 |
    10 | <%= form_for @campaign do | campaign | %> 11 |
    12 | <%= campaign.label :title, "Title" %> 13 | <%= campaign.text_field :title, class: "form-control input-lg", placeholder: "Help me pay my college tuition!", value: @campaign.title || @campaign_title %> 14 |
    15 |
    16 | <%= campaign.label :goal, "Goal ($20 - $1,000,000)" %> 17 |
    18 | $ 19 | <%= campaign.text_field :goal, class: "form-control input-lg", placeholder: "200.00", value: @campaign.goal || @goal %> 20 |
    21 |
    22 |
    23 | <%= campaign.label :description, "Description" %> 24 | <%= campaign.text_area :description, class: "form-control input-lg", placeholder: "My tuition is expensive! Please donate if you want to help me pay it.", value: @campaign.description || @campaign_description, rows: 4 %> 25 |
    26 |
    27 | <%= campaign.label :image, "Select an image for your campaign" %> 28 | <%= render 'images' %> 29 | <%= campaign.hidden_field :image, value: @campaign.image || @campaign_image %> 30 |
    31 |
    32 | <%= campaign.button "Create Campaign", class: "btn btn-primary btn-lg btn-block btn-custom", data: {disable_with: " Creating campaign..."} %> 33 |
    34 | <% end %> 35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 | -------------------------------------------------------------------------------- /app/controllers/debit_cards_controller.rb: -------------------------------------------------------------------------------- 1 | class DebitCardsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def new 5 | end 6 | 7 | def create 8 | # Redirect if no token is POSTed or the user doesn't have a Stripe account 9 | unless params[:stripeToken] && current_user.stripe_account 10 | redirect_to debit_cards_create_path and return 11 | end 12 | 13 | begin 14 | # Retrieve the account object for this user 15 | account = Stripe::Account.retrieve(current_user.stripe_account) 16 | 17 | # Create the bank account 18 | account.external_accounts.create(external_account: params[:stripeToken]) 19 | account.save 20 | 21 | # Success, send on to the dashboard 22 | flash[:success] = "Your debit card has been added!" 23 | redirect_to dashboard_path 24 | 25 | # Handle exceptions from Stripe 26 | rescue Stripe::StripeError => e 27 | handle_error(e.message, 'new') 28 | 29 | # Handle any other exceptions 30 | rescue => e 31 | handle_error(e.message, 'new') 32 | end 33 | end 34 | 35 | def destroy 36 | end 37 | 38 | def transfer 39 | begin 40 | # Retrieve the account object for this user 41 | account = Stripe::Account.retrieve(current_user.stripe_account) 42 | 43 | # Create an instant payout 44 | payout = Stripe::Payout.create( 45 | { 46 | amount: params[:amount], 47 | currency: "usd", 48 | method: "instant", 49 | destination: params[:destination] 50 | }, 51 | { stripe_account: account.id } 52 | ) 53 | 54 | # Take a 3% fee for the instant payout 55 | Stripe::Charge.create( 56 | amount: params[:fee], 57 | currency: "usd", 58 | source: account.id, 59 | description: "Instant payout fee for #{payout.id}" 60 | ) 61 | 62 | # Success, send on to the dashboard 63 | flash[:success] = "Your payout has been made!" 64 | redirect_to dashboard_path 65 | 66 | # Handle exceptions from Stripe 67 | rescue Stripe::StripeError => e 68 | flash[:error] = e.message 69 | redirect_to dashboard_path 70 | 71 | # Handle any other exceptions 72 | rescue => e 73 | flash[:error] = e.message 74 | redirect_to dashboard_path 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /app/views/campaigns/dashboard.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Dashboard' %> 2 |
    3 |
    4 |
    5 | <%= render 'layouts/messages' %> 6 |
    7 |
    8 | <% unless @stripe_account.requirements.currently_due.empty? %> 9 | <%= render 'verification' %> 10 | <% end %> 11 | 12 | <% unless @campaigns.empty? %> 13 | <%= render 'campaigns' %> 14 | <% else %> 15 | <%= link_to "Create your first fundraising campaign to get started", new_campaign_path %>. 16 | <% end %> 17 | 21 |
    22 |
    23 | <% unless @payments.data.empty? %> 24 | <%= render 'charges' %> 25 | <% if @balance_available > 0 %> 26 |
    27 |

    Want your funds immediately?

    28 |

    29 | For an extra 3% fee, we'll create an instant transfer to your debit card. 30 |

    31 | <% if @debit_card %> 32 | <%= button_to "Get this transfer instantly", instant_transfer_path, data: { confirm: "Instant payout #{format_amount(@instant_amt)} to your #{@debit_card.brand} debit card ending in #{@debit_card.last4}?", disable_with: "Transferring funds..." }, method: :post, params: { amount: @instant_amt.round, fee: @instant_fee.round, destination: @debit_card.id }, class: "btn btn-success btn-lg btn-custom" %> 33 | <% else %> 34 | <%= link_to "Add a debit card", debit_cards_new_path, class: "btn btn-lg btn-success btn-custom" %> 35 | <% end %> 36 |
    37 | <% end %> 38 | <% else %> 39 | No donations yet. 40 | <% end %> 41 |
    42 |
    43 | 44 |
    45 |
    46 | <% unless @payouts.data.empty? %> 47 | <%= render 'transfers' %> 48 | <% else %> 49 | No payouts yet. 50 | <% end %> 51 |
    52 |
    53 |
    54 | -------------------------------------------------------------------------------- /app/views/devise/registrations/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Your account' %> 2 |
    3 |
    4 |
    5 |

    Manage your account

    6 | <%= devise_error_messages! %> 7 | <%= render 'layouts/messages' %> 8 |
    9 |
    10 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> 11 | <%= devise_error_messages! %> 12 | 13 |
    14 | <%= f.label :email %> 15 | <%= f.email_field :email, class: 'form-control input-lg', placeholder: "Email", autofocus: true %> 16 |
    17 | 18 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> 19 |
    Currently waiting confirmation for: <%= resource.unconfirmed_email %>
    20 | <% end %> 21 | 22 |
    23 | <%= f.password_field :password, autocomplete: "off", class: 'form-control input-lg', placeholder: "New password" %> 24 | Leave blank if you don't want to change your password 25 |
    26 | 27 |
    28 | <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control input-lg', placeholder: "Confirm new password" %> 29 |
    30 | 31 |
    32 | <%= f.password_field :current_password, autocomplete: "off", class: 'form-control input-lg', placeholder: "Current password" %> 33 |
    34 | 35 |
    36 | <%= f.submit "Update", class: "btn btn-primary btn-lg btn-block btn-custom", data: {disable_with: " Updating your account..."} %> 37 |
    38 | <% end %> 39 |
    40 |
    41 |
    42 |
    43 |
    44 |
    45 |

    46 | <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure you want to cancel your account? You can't undo this" }, method: :delete, class: "btn btn-danger btn-lg btn-block btn-custom" %> 47 |

    48 |

    49 | <%= link_to "Back", :back %> 50 |

    51 |
    52 |
    53 |
    54 | -------------------------------------------------------------------------------- /app/views/campaigns/_charges.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% @payments.each do |payment| %> 15 | <% if payment.source_transfer.source_transaction.present? %> 16 | 17 | 32 | 33 | 34 | 35 | 44 | 45 | <% end %> 46 | <% end %> 47 | 48 |
    Payment IDAmountNetCreatedActions
    18 | <% if payment.source_transfer.source_transaction.source.object.eql?('card') %> 19 | 20 | <% end %> 21 | <%= link_to payment.source_transfer.source_transaction.id, charge_path(payment.source_transfer.source_transaction.id), class: "leftspace" %> 22 | <% if payment.source_transfer.source_transaction.dispute.present? %> 23 | <% if payment.source_transfer.source_transaction.dispute.status.eql?('needs_response') %> 24 | Disputed 25 | <% elsif payment.source_transfer.source_transaction.dispute.status.eql?('lost') %> 26 | Dispute lost 27 | <% elsif payment.source_transfer.source_transaction.dispute.status.eql?('won') %> 28 | Dispute won 29 | <% end %> 30 | <% end %> 31 | <%= number_to_currency(payment.amount/100) %><%= format_amount(payment.amount - payment.application_fee.amount) %><%= format_date(payment.created) %> 36 | <% if payment.refunded %> 37 | 38 | Refunded 39 | 40 | <% else %> 41 | <%= link_to "Refund", charge_path(payment.source_transfer.source_transaction.id), method: :delete, data: { confirm: "Are you sure you want to refund this charge?", disable_with: " Refunding charge..." }, class: "btn btn-sm btn-block btn-custom btn-danger" %> 42 | <% end %> 43 |
    49 |
    50 |
    51 | -------------------------------------------------------------------------------- /app/views/bank_accounts/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, "Add a new bank account" %> 2 | <% content_for(:header) do %> 3 | 4 | 5 | 49 | <% end %> 50 |
    51 |
    52 |
    53 |

    Add a bank account

    54 |
    55 | <%= render 'layouts/messages' %> 56 | <%= render 'bank_account_form' %> 57 |
    58 |

    59 | Hint: use 110000000 and 000123456789 to simulate a successful transfer. 60 |

    61 |

    62 | Other test bank account numbers 63 |

    64 |
    65 |
    66 |
    67 |
    68 |
    69 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20170502170520) do 14 | 15 | create_table "campaigns", force: :cascade do |t| 16 | t.integer "user_id" 17 | t.string "title" 18 | t.text "description" 19 | t.boolean "subscription" 20 | t.datetime "created_at", null: false 21 | t.datetime "updated_at", null: false 22 | t.string "image" 23 | t.integer "goal" 24 | t.integer "raised" 25 | t.boolean "active", default: true 26 | end 27 | 28 | create_table "charges", force: :cascade do |t| 29 | t.string "charge_id" 30 | t.integer "amount" 31 | t.integer "amount_refunded" 32 | t.integer "campaign_id" 33 | t.datetime "created_at", null: false 34 | t.datetime "updated_at", null: false 35 | t.string "name" 36 | end 37 | 38 | create_table "stripe_accounts", force: :cascade do |t| 39 | t.string "first_name" 40 | t.string "last_name" 41 | t.string "account_type" 42 | t.integer "dob_month" 43 | t.integer "dob_day" 44 | t.integer "dob_year" 45 | t.datetime "created_at", null: false 46 | t.datetime "updated_at", null: false 47 | t.string "address_city" 48 | t.string "address_state" 49 | t.string "address_line1" 50 | t.string "address_postal" 51 | t.boolean "tos" 52 | t.string "ssn_last_4" 53 | t.string "business_name" 54 | t.string "business_tax_id" 55 | t.string "personal_id_number" 56 | t.string "verification_document" 57 | t.string "acct_id" 58 | end 59 | 60 | create_table "users", force: :cascade do |t| 61 | t.string "email", default: "", null: false 62 | t.string "encrypted_password", default: "", null: false 63 | t.string "reset_password_token" 64 | t.datetime "reset_password_sent_at" 65 | t.datetime "remember_created_at" 66 | t.integer "sign_in_count", default: 0, null: false 67 | t.datetime "current_sign_in_at" 68 | t.datetime "last_sign_in_at" 69 | t.string "current_sign_in_ip" 70 | t.string "last_sign_in_ip" 71 | t.datetime "created_at", null: false 72 | t.datetime "updated_at", null: false 73 | t.string "stripe_account" 74 | t.index ["email"], name: "index_users_on_email", unique: true 75 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /app/views/payouts/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, "View payout" %> 2 |
    3 |
    4 |
    5 |

    Payout details

    6 |
    7 | <%= render 'layouts/messages' %> 8 |
    9 |
    10 |
    11 | Payout overview 12 |
    13 |
    14 |
    Date paid
    15 |
    <%= format_date(@payout.created) %>
    16 |
    17 |
    18 |
    Payout amount
    19 |
    <%= format_amount(@payout.amount) %>
    20 |
    21 | <% if @payout.bank_account %> 22 |
    23 |
    Bank account
    24 |
    <%= "#{@payout.bank_account.bank_name} ending in #{@payout.bank_account.last4}" %>
    25 |
    26 | <% end %> 27 |
    28 |
    29 |
    30 |
    31 | Payout transactions 32 |
    33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | <% @txns.auto_paging_each do |txn| %> 44 | <% unless txn.type.eql?('transfer') && txn.source.type.eql?('bank_account') %> 45 | 46 | 63 | 64 | 65 | 66 | 67 | <% end %> 68 | <% end %> 69 | 70 |
    TypeGrossFeeTotal
    47 | <% if txn.type.eql?('payment') && txn.source.source_transfer.source_transaction.present? %> 48 | <%= link_to "Payment", charge_path(txn.source.source_transfer.source_transaction) %> 49 | <% elsif txn.type.eql?('payment_refund') %> 50 | <%= link_to "Payment refund", charge_path(txn.source.charge.source_transfer.source_transaction) %> 51 | <% elsif txn.type.eql?('adjustment') %> 52 | Marketplace fee refund 53 | <% elsif txn.type.eql?('transfer') && txn.source.method.eql?('instant') %> 54 | Instant payout <%= txn.source.id %> 55 | <% elsif txn.type.eql?('transfer') && txn.source.type.eql?('stripe_account') %> 56 | <%= txn.description %> 57 | <% elsif txn.description.present? %> 58 | <%= txn.description %> 59 | <% else %> 60 | <%= txn.type %> 61 | <% end %> 62 | <%= format_amount(txn.amount) %><%= format_amount(txn.fee) %><%= format_amount(txn.net) %>
    71 |
    72 |
    73 |
    74 |
    75 |
    76 | -------------------------------------------------------------------------------- /app/controllers/webhooks_controller.rb: -------------------------------------------------------------------------------- 1 | class WebhooksController < ApplicationController 2 | protect_from_forgery except: :stripe 3 | 4 | def stripe 5 | # Use signed webhooks 6 | endpoint_secret = ENV['ENDPOINT_SECRET'].to_s 7 | 8 | payload = request.body.read 9 | sig_header = request.env['HTTP_STRIPE_SIGNATURE'] 10 | event = nil 11 | 12 | begin 13 | event = Stripe::Webhook.construct_event( 14 | payload, sig_header, endpoint_secret 15 | ) 16 | 17 | case event.type 18 | 19 | ############# 20 | # Disputes 21 | ############# 22 | 23 | # Recover dispute amounts and fees for disputed charges 24 | when 'charge.dispute.created' 25 | # The dispute that was created 26 | dispute = event.data.object 27 | 28 | # Retrieve the charge related to this dispute 29 | charge = Stripe::Charge.retrieve(dispute.charge) 30 | 31 | # Retrieve the platform account ID. You could also store this in an env variable. 32 | platform_account = Stripe::Account.retrieve 33 | 34 | # Issue a transfer reversal to recover the funds 35 | reverse_transfer(charge) 36 | 37 | # Create an account debit to recover dispute fee 38 | debit = Stripe::Transfer.create( 39 | { 40 | amount: dispute.balance_transactions.first.fee, 41 | currency: "usd", 42 | destination: platform_account.id, 43 | description: "Dispute fee for #{charge.id}" 44 | }, 45 | { stripe_account: charge.destination } 46 | ) 47 | 48 | # Return charge amount and fees if dispute is won 49 | when 'charge.dispute.funds_reinstated' 50 | # The dispute that was created 51 | dispute = event.data.object 52 | 53 | # Retrieve the charge related to this dispute 54 | charge = Stripe::Charge.retrieve(dispute.charge) 55 | 56 | # Create a transfer to the connected account to return the dispute fee 57 | transfer = Stripe::Transfer.create( 58 | amount: dispute.balance_transactions.second.net, 59 | currency: "usd", 60 | destination: charge.destination 61 | ) 62 | 63 | # Retrieve the destination payment 64 | payment = Stripe::Charge.retrieve( 65 | { 66 | id: transfer.destination_payment 67 | }, 68 | { 69 | stripe_account: transfer.destination 70 | } 71 | ) 72 | 73 | # Update the description on the destination payment 74 | payment.description = "Chargeback reversal for #{charge.id}" 75 | payment.save 76 | 77 | end 78 | 79 | # Something bad happened with the event or retrieving details from Stripe: probably log this. 80 | rescue JSON::ParserError, Stripe::SignatureVerificationError, Stripe::StripeError => e 81 | head :bad_request 82 | 83 | # Handle other exceptions. You may want to log these for review later too. 84 | rescue => e 85 | head :bad_request 86 | end 87 | 88 | # Return a 200 89 | head :ok 90 | end 91 | 92 | private 93 | def reverse_transfer(charge) 94 | # Retrieve the transfer for the charge 95 | transfer = Stripe::Transfer.retrieve(charge.transfer) 96 | 97 | # Reverse the transfer and keep the application fee 98 | transfer.reversals.create 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /app/views/charges/_dispute.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Respond to this dispute

    3 |
    4 |
    5 |
    6 |
    7 |
    8 |
    9 | 10 | 11 |
    12 | Share any communication with the customer or feedback on why this donation shouldn't have been disputed 13 |
    14 |
    15 |
    16 | 17 | 18 | Upload any supporting documentation (optional) 19 |
    20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 |
    28 | <%= hidden_field_tag :authenticity_token, form_authenticity_token -%> 29 | <%= hidden_field_tag :dispute_id, @charge.dispute.id -%> 30 | 31 |
    32 |
    33 | 34 | 35 | 86 | -------------------------------------------------------------------------------- /app/views/charges/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, 'Fundraising Marketplace | Review charge' %> 2 |
    3 |
    4 |
    5 | <%= render 'layouts/messages' %> 6 |
    7 |
    8 |
    9 |
    10 | 13 |
    14 |
    15 |
    16 |
    From
    17 |
    <%= @charge.metadata.name %>
    18 |
    19 |
    Payment method
    20 |
    <%= @charge.source.brand %> ending in <%= @charge.source.last4 %>
    21 |
    22 |
    For
    23 |
    <%= link_to @campaign.title, campaign_path(@campaign) %>
    24 |
    25 |
    Marketplace fee
    26 |
    <%= format_amount(@charge.application_fee.amount) %>
    27 |
    28 |
    Net earnings
    29 |
    <%= format_amount(@charge.amount - @charge.application_fee.amount) %>
    30 |
    31 |
    Created
    32 |
    <%= format_date(@charge.created) %>
    33 |
    34 |
    Refunded
    35 |
    <%= @charge.refunded %>
    36 |
    37 |
    Disputed
    38 |
    <%= @charge.dispute.present? %>
    39 |
    40 |
    41 | <% if @charge.refunded %> 42 | 43 | <% elsif @charge.dispute.present? %> 44 |
    45 | <% if @charge.dispute.status.eql?('needs_response') %> 46 | Needs response 47 | This donation was disputed. <%= format_amount(@charge.dispute.balance_transactions.first.net.abs) %> in fees and disputed funds have been withdrawn from your account. 48 | <% elsif @charge.dispute.status.eql?('won') %> 49 | Won 50 | This dispute was won in your favor. <%= format_amount(@charge.dispute.balance_transactions.first.net.abs) %> in fees and disputed funds have been returned to your account. 51 | <% elsif @charge.dispute.status.eql?('lost') %> 52 | Lost 53 | The bank sided in favor of the donor. <%= format_amount(@charge.dispute.balance_transactions.first.net.abs) %> in fees and disputed funds have been withdrawn from your account. 54 | <% end %> 55 |
    56 | <% else %> 57 | <%= link_to "Refund this donation", charge_path(@charge.id), method: :delete, data: { confirm: "Are you sure you want to refund this charge?", disable_with: " Refunding charge..." }, class: "btn btn-lg btn-block btn-custom btn-danger" %> 58 | <% end %> 59 |
    60 |
    61 | <% if @charge.dispute.present? && (@charge.dispute.status.eql?('needs_response') || @charge.dispute.status.eql?('under_review')) %> 62 | <%= render 'dispute' %> 63 | <% end %> 64 |
    65 |
    66 |
    67 | -------------------------------------------------------------------------------- /app/views/debit_cards/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, "Add debit card" %> 2 | <% content_for(:header) do %> 3 | 4 | 79 | <% end %> 80 |
    81 |
    82 |
    83 |

    Add a debit card

    84 |

    Get an instant payout

    85 |
    86 | <%= render 'layouts/messages' %> 87 | <%= render 'debit_card_form' %> 88 |
    89 |

    90 | Hint: use 4000056655665556 and any valid expiry. 91 |

    92 |

    93 | See all test debit card numbers 94 |

    95 |
    96 |
    97 |
    98 |
    99 |
    -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | # Compress JavaScripts and CSS. 22 | config.assets.js_compressor = :uglifier 23 | # config.assets.css_compressor = :sass 24 | 25 | # Do not fallback to assets pipeline if a precompiled asset is missed. 26 | config.assets.compile = false 27 | 28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 29 | 30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 31 | # config.action_controller.asset_host = 'http://assets.example.com' 32 | 33 | # Specifies the header that your server uses for sending files. 34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 36 | 37 | # Mount Action Cable outside main process or domain 38 | # config.action_cable.mount_path = nil 39 | # config.action_cable.url = 'wss://example.com/cable' 40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Use the lowest log level to ensure availability of diagnostic information 46 | # when problems arise. 47 | config.log_level = :debug 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [ :request_id ] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Use a real queuing backend for Active Job (and separate queues per environment) 56 | # config.active_job.queue_adapter = :resque 57 | # config.active_job.queue_name_prefix = "seller-dashboard_#{Rails.env}" 58 | config.action_mailer.perform_caching = false 59 | 60 | # Ignore bad email addresses and do not raise email delivery errors. 61 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 62 | # config.action_mailer.raise_delivery_errors = false 63 | 64 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 65 | # the I18n.default_locale when a translation cannot be found). 66 | config.i18n.fallbacks = true 67 | 68 | # Send deprecation notices to registered listeners. 69 | config.active_support.deprecation = :notify 70 | 71 | # Use default logging formatter so that PID and timestamp are not suppressed. 72 | config.log_formatter = ::Logger::Formatter.new 73 | 74 | # Use a different logger for distributed setups. 75 | # require 'syslog/logger' 76 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 77 | 78 | if ENV["RAILS_LOG_TO_STDOUT"].present? 79 | logger = ActiveSupport::Logger.new(STDOUT) 80 | logger.formatter = config.log_formatter 81 | config.logger = ActiveSupport::TaggedLogging.new(logger) 82 | end 83 | 84 | # Do not dump schema after migrations. 85 | config.active_record.dump_schema_after_migration = false 86 | end 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fundraising Marketplace: A Stripe Connect example app 2 | 3 | An example application built using [Stripe Connect](https://stripe.com/docs/connect) [custom accounts](https://stripe.com/docs/connect/custom-accounts). **This application is provided as an example, but isn't meant to be run in production.** 4 | 5 | [![Dashboard demo](https://i.imgur.com/2YvhiaL.png)](https://stripe-marketplace-demo.herokuapp.com) 6 | 7 | [![Payout demo](https://i.imgur.com/6s5vm9A.png)](https://stripe-marketplace-demo.herokuapp.com) 8 | 9 | [![Charge view demo](https://i.imgur.com/GO0SsHL.png)](https://stripe-marketplace-demo.herokuapp.com) 10 | 11 | 12 | ## Demo 13 | **You can find a working demo of this application deployed and running in test mode at https://stripe-marketplace-demo.herokuapp.com/**. Feel free to create an account, create a campaign, and make donations to see some data populated in the dashboard. You can find [test card numbers](https://stripe.com/docs/testing#cards), [bank accounts](https://stripe.com/docs/testing#managed-accounts), and [identity verification](https://stripe.com/docs/connect/testing) details in Stripe's documentation. 14 | 15 | 16 | ## Features 17 | :money_with_wings: Create fundraising campaigns and custom Stripe Connect accounts. 18 | 19 | :lock: Uses [Devise](https://github.com/plataformatec/devise) for user authentication. 20 | 21 | :chart_with_upwards_trend: Fairly complete seller dashboard to view charges, create refunds, view payouts, etc. 22 | 23 | :iphone: Fully responsive for mobile browsers. 24 | 25 | :bank: Add and modify connected [bank accounts](https://stripe.com/docs/api#account_create_bank_account). 26 | 27 | :credit_card: Make donations with either [Stripe Elements](https://stripe.com/docs/elements) or [Stripe Checkout](https://stripe.com/docs/checkout). [Make successful donations](https://stripe.com/docs/testing#cards) using valid test card numbers or see declines using test cards. 28 | 29 | :sunglasses: Take a 10% [application fee](https://stripe.com/docs/connect/charges) from connected accounts for successful charges. 30 | 31 | :zap: Create payouts to debit cards using [instant payouts](https://stripe.com/docs/connect/payouts#instant-payouts) and take a 3% platform fee in return using [account debits](https://stripe.com/docs/connect/account-debits). 32 | 33 | :clipboard: Identity verification example form and dashboard prompt to work through the [identity verification](https://stripe.com/docs/connect/identity-verification) process. Includes examples of collecting all info up front vs incrementally. 34 | 35 | :poop: [Create disputes](https://stripe.com/docs/testing#disputes) and use [webhooks](https://stripe.com/docs/webhooks) to recover funds + dispute fees automatically via account debits. 36 | 37 | :arrow_right_hook: Includes [webhook signature validation](https://stripe.com/docs/webhooks#signatures) for enhanced security. 38 | 39 | 40 | ## Shortcomings, things still needed 41 | * Still pretty basic integration tests. 42 | * Email receipts/notifications/etc. 43 | * Additional features like pagination for charges/transfers, ACH payments, alternative payment methods, etc. 44 | 45 | 46 | ## Specs 47 | Built on Rails 5 and running Ruby 2.2. Uses the 2019-02-19 API version, which includes [major chages](https://stripe.com/docs/upgrades#2019-02-19) to the way Connect onboarding works. 48 | 49 | 50 | ## Setup 51 | To run this locally, clone the repository and run bundler to install dependencies: 52 | 53 | ``` 54 | git clone https://github.com/adam-stripe/stripe-connect-managed-rails.git 55 | cd stripe-connect-managed-rails 56 | bundle install 57 | ``` 58 | 59 | Migrate: 60 | 61 | ``` 62 | $ rails db:migrate 63 | ``` 64 | 65 | Retrieve your [Stripe API keys](https://dashboard.stripe.com/account/apikeys) and set them as environment variables. You can run this app locally by starting Rails server: 66 | 67 | ``` 68 | PUBLISHABLE_KEY=YOUR_STRIPE_PUBLISHABLE_KEY SECRET_KEY=YOUR_STRIPE_SECRET_KEY ENDPOINT_SECRET=YOUR_WEBHOOK_ENDPOINT_SECRET rails s 69 | ``` 70 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | password_change: 27 | subject: "Password Changed" 28 | omniauth_callbacks: 29 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 30 | success: "Successfully authenticated from %{kind} account." 31 | passwords: 32 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 33 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 34 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 35 | updated: "Your password has been changed successfully. You are now signed in." 36 | updated_not_active: "Your password has been changed successfully." 37 | registrations: 38 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 39 | signed_up: "Welcome! Add your information below to create a fundraising campaign." 40 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 41 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 42 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." 43 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." 44 | updated: "Your account has been updated successfully." 45 | sessions: 46 | signed_in: "Signed in successfully." 47 | signed_out: "Signed out successfully." 48 | already_signed_out: "Signed out successfully." 49 | unlocks: 50 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 51 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 52 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 53 | errors: 54 | messages: 55 | already_confirmed: "was already confirmed, please try signing in" 56 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 57 | expired: "has expired, please request a new one" 58 | not_found: "not found" 59 | not_locked: "was not locked" 60 | not_saved: 61 | one: "1 error prohibited this %{resource} from being saved:" 62 | other: "%{count} errors prohibited this %{resource} from being saved:" 63 | -------------------------------------------------------------------------------- /app/views/stripe_accounts/_account_form.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <%= form_for @account do | f | %> 4 |
    5 |
    6 |
    7 | <%= f.label :account_type, "Account Type" %> 8 | <%= f.select(:account_type, options_for_select([["Individual","individual"], ["Company","company"]], @account[:account_type]), {}, { class: "form-control input-lg" }) %> 9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 | <%= f.label :business_name, "Business name (as it appears to the IRS)" %> 17 | <%= f.text_field :business_name, class: "form-control input-lg", placeholder: "My Business LLC" %> 18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 | <%= f.label :business_tax_id, "Business tax ID/EIN" %> 25 | <%= f.text_field :business_tax_id, class: "form-control input-lg", placeholder: "27-0000000" %> 26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 |
    33 | <%= f.label :first_name, "First Name" %> 34 | <%= f.text_field :first_name, class: "form-control input-lg", placeholder: "Jane" %> 35 |
    36 |
    37 |
    38 |
    39 | <%= f.label :last_name, "Last Name" %> 40 | <%= f.text_field :last_name, class: "form-control input-lg", placeholder: "Doe" %> 41 |
    42 |
    43 |
    44 |
    45 |
    46 |
    47 | <%= f.label :ssn_last_4, "Last 4 digits of your SSN" %> 48 | <%= f.text_field :ssn_last_4, class: "form-control input-lg", placeholder: "6789" %> 49 |
    50 |
    51 |
    52 |
    53 |
    54 |
    55 |
    56 | <%= f.label :dob_month, "DOB Month" %> 57 | <%= f.select(:dob_month, options_for_select([["January", "1"],["February", "2"],["March", "3"],["April", "4"],["May", "5"],["June", "6"],["July", "7"],["August", "8"],["September", "9"],["October", "10"],["November", "11"],["December", "12"]], @account[:dob_month]), {}, { class: "form-control input-lg" }) %> 58 |
    59 |
    60 |
    61 |
    62 | <%= f.label :dob_day, "DOB Day" %> 63 | <%= f.select(:dob_day, options_for_select((1..31).each{|n| [n, n]}, @account[:dob_day]), {}, { class: "form-control input-lg" }) %> 64 |
    65 |
    66 |
    67 |
    68 | <%= f.label :dob_year, "DOB Year" %> 69 | <%= f.select(:dob_year, options_for_select((1912..2016).each{|n| [n, n]}, @account[:dob_year]), {}, { class: "form-control input-lg" }) %> 70 |
    71 |
    72 |
    73 |
    74 |
    75 |
    76 |
    77 | <%= f.check_box :tos %> 78 | <%= f.label :tos do %> 79 | I agree to the <%= link_to "terms of service", terms_path %>. 80 | <% end %> 81 |
    82 |
    83 |
    84 |
    85 |
    86 |
    87 | <%= f.button "Create Account", class: "btn btn-primary btn-lg btn-block btn-custom", data: {disable_with: " Creating account..."} %> 88 |
    89 |
    90 |
    91 | <% end %> 92 |
    93 |
    94 | 105 | -------------------------------------------------------------------------------- /app/controllers/charges_controller.rb: -------------------------------------------------------------------------------- 1 | class ChargesController < ApplicationController 2 | before_action :authenticate_user!, only: [:show, :destroy] 3 | 4 | def create 5 | # Check for a stripeToken 6 | unless charge_params[:stripeToken] 7 | flash[:error] = "No payment information submitted." 8 | redirect_back(fallback_location: root_path) and return 9 | end 10 | 11 | # Check for a valid campaign ID 12 | unless charge_params[:campaign] && Campaign.exists?(charge_params[:campaign]) 13 | flash[:error] = "The campaign you specified doesn't exist." 14 | redirect_back(fallback_location: root_path) and return 15 | end 16 | 17 | # Retrieve the campaign 18 | campaign = Campaign.find(params[:campaign]) 19 | 20 | begin 21 | # Find the account ID associated with this campaign 22 | account_id = User.find(campaign.user_id).stripe_account 23 | 24 | # Convert the amount to cents 25 | amount = (100 * charge_params[:amount].tr('$', '').to_r).to_i 26 | 27 | # Create the charge with Stripe 28 | charge = Stripe::Charge.create({ 29 | source: charge_params[:stripeToken], 30 | amount: amount, 31 | currency: "usd", 32 | transfer_data: { 33 | destination: account_id, 34 | }, 35 | application_fee_amount: amount/10, # Take a 10% application fee for the platform 36 | metadata: { "name" => charge_params[:name], "campaign" => campaign.id } 37 | } 38 | ) 39 | 40 | # Save the charge details locally for later use 41 | local_charge = Charge.create( 42 | charge_id: charge.id, 43 | amount: amount, 44 | name: charge_params[:name], 45 | campaign_id: campaign.id 46 | ) 47 | 48 | # Update the amount raised for this campaign 49 | campaign.raised = campaign.raised.to_i + amount 50 | campaign.save 51 | 52 | # Let the customer know the payment was successful 53 | flash[:success] = "Thanks for your donation! Your transaction ID is #{charge.id}." 54 | redirect_to campaign_path(campaign) 55 | 56 | # Handle exceptions from Stripe 57 | rescue Stripe::StripeError => e 58 | flash[:error] = e.message 59 | redirect_to campaign_path(campaign) 60 | 61 | # Handle other failures 62 | rescue => e 63 | flash[:error] = e.message 64 | redirect_to campaign_path(campaign) 65 | end 66 | end 67 | 68 | def show 69 | begin 70 | # Retrieve the charge from Stripe 71 | @charge = Stripe::Charge.retrieve(id: params[:id], expand: ['application_fee', 'dispute']) 72 | 73 | # Validate that the user should be able to view this charge 74 | check_destination(@charge) 75 | 76 | # Get the campaign from the metadata on the charge object 77 | @campaign = Campaign.find(@charge.metadata.campaign) 78 | 79 | # Handle exceptions from Stripe 80 | rescue Stripe::StripeError => e 81 | flash[:error] = e.message 82 | redirect_to dashboard_path 83 | 84 | # Handle other failures 85 | rescue => e 86 | flash[:error] = e.message 87 | redirect_to dashboard_path 88 | end 89 | end 90 | 91 | def index 92 | end 93 | 94 | # Using destroy action to handle refunds 95 | def destroy 96 | begin 97 | # Retrieve the charge from Stripe 98 | charge = Stripe::Charge.retrieve(params[:id]) 99 | 100 | # Validate that the user should be able to view this charge 101 | check_destination(charge) 102 | 103 | # Refund the charge 104 | charge.refund(reverse_transfer: true, refund_application_fee: true) 105 | 106 | # Update the local charge 107 | local_charge = Charge.find_by charge_id: charge.id 108 | local_charge.amount_refunded = charge.amount 109 | local_charge.save 110 | 111 | # Update the amount raised for this campaign 112 | campaign = Campaign.find(local_charge.campaign_id) 113 | campaign.raised = campaign.raised.to_i - charge.amount 114 | campaign.save 115 | 116 | # Let the user know the refund was successful 117 | flash[:success] = "The charge has been refunded." 118 | redirect_to dashboard_path 119 | 120 | # Handle Stripe exceptions 121 | rescue Stripe::StripeError => e 122 | flash.now[:error] = e.message 123 | redirect_to dashboard_path 124 | 125 | # Handle other failures 126 | rescue => e 127 | flash.now[:error] = e.message 128 | redirect_to dashboard_path 129 | end 130 | end 131 | 132 | private 133 | def charge_params 134 | params.permit(:amount, :stripeToken, :name, :campaign, :authenticity_token) 135 | end 136 | 137 | # Charge is retrieved from the platform, so only the destination should have access to it 138 | def check_destination(charge) 139 | # Redirect to the dashboard if the charge doesn't belong to current user 140 | unless charge.destination.eql?(current_user.stripe_account) 141 | flash[:error] = "You don't have access to this charge." 142 | redirect_to dashboard_path 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components.scss: -------------------------------------------------------------------------------- 1 | /* =========== 2 | Buttons 3 | =============*/ 4 | .btn { 5 | border-radius: 2px; 6 | padding: 6px 14px; 7 | webkit-box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4); 8 | } 9 | .btn-group-lg > .btn, 10 | .btn-lg { 11 | padding: 10px 16px !important; 12 | font-size: 16px; 13 | } 14 | .btn-group-sm > .btn, 15 | .btn-sm { 16 | padding: 5px 10px !important; 17 | } 18 | .btn-group-xs > .btn, 19 | .btn-xs { 20 | padding: 1px 5px !important; 21 | } 22 | .btn-group .btn + .btn, 23 | .btn-group .btn + .btn-group, 24 | .btn-group .btn-group + .btn, 25 | .btn-group .btn-group + .btn-group { 26 | margin-left: 0px; 27 | } 28 | .btn-group.open .dropdown-toggle { 29 | -webkit-box-shadow: 0 0 0 100px rgba(0, 0, 0, 0.1) inset; 30 | box-shadow: 0 0 0 100px rgba(0, 0, 0, 0.1) inset; 31 | } 32 | .btn-primary, 33 | .btn-success, 34 | .btn-info, 35 | .btn-warning, 36 | .btn-danger, 37 | .btn-inverse, 38 | .btn-purple, 39 | .btn-pink { 40 | color: #ffffff !important; 41 | } 42 | .btn-default { 43 | background-color: #dae6ec; 44 | border-color: #dae6ec; 45 | } 46 | .btn-default:focus { 47 | background-color: #dae6ec; 48 | border-color: #C2CED4; 49 | } 50 | .btn-default:hover { 51 | background-color: #dae6ec; 52 | border-color: #C2CED4; 53 | } 54 | .btn-default:active { 55 | background-color: #dae6ec; 56 | border-color: #C2CED4; 57 | } 58 | .btn-default.active, 59 | .btn-default:active, 60 | .open > .dropdown-toggle.btn-default { 61 | background-color: #dae6ec !important; 62 | border-color: #C2CED4 !important; 63 | } 64 | .btn-primary { 65 | background-color: #3bafda !important; 66 | border: 1px solid #3bafda !important; 67 | } 68 | .btn-primary:hover, 69 | .btn-primary:focus, 70 | .btn-primary:active, 71 | .btn-primary.active, 72 | .btn-primary.focus, 73 | .btn-primary:active, 74 | .btn-primary:focus, 75 | .btn-primary:hover, 76 | .open > .dropdown-toggle.btn-primary { 77 | background-color: #28a5d4 !important; 78 | border: 1px solid #28a5d4 !important; 79 | } 80 | .btn-success { 81 | background-color: #00b19d !important; 82 | border: 1px solid #00b19d !important; 83 | } 84 | .btn-success:hover, 85 | .btn-success:focus, 86 | .btn-success:active, 87 | .btn-success.active, 88 | .btn-success.focus, 89 | .btn-success:active, 90 | .btn-success:focus, 91 | .btn-success:hover, 92 | .open > .dropdown-toggle.btn-success { 93 | background-color: #009886 !important; 94 | border: 1px solid #009886 !important; 95 | } 96 | .btn-info { 97 | background-color: #3ddcf7 !important; 98 | border: 1px solid #3ddcf7 !important; 99 | } 100 | .btn-info:hover, 101 | .btn-info:focus, 102 | .btn-info:active, 103 | .btn-info.active, 104 | .btn-info.focus, 105 | .btn-info:active, 106 | .btn-info:focus, 107 | .btn-info:hover, 108 | .open > .dropdown-toggle.btn-info { 109 | background-color: #25d8f6 !important; 110 | border: 1px solid #25d8f6 !important; 111 | } 112 | .btn-warning { 113 | background-color: #ffaa00 !important; 114 | border: 1px solid #ffaa00 !important; 115 | } 116 | .btn-warning:hover, 117 | .btn-warning:focus, 118 | .btn-warning:active, 119 | .btn-warning.active, 120 | .btn-warning.focus, 121 | .btn-warning:active, 122 | .btn-warning:focus, 123 | .btn-warning:hover, 124 | .open > .dropdown-toggle.btn-warning { 125 | background-color: #e69900 !important; 126 | border: 1px solid #e69900 !important; 127 | } 128 | .btn-danger { 129 | background-color: #ef5350 !important; 130 | border: 1px solid #ef5350 !important; 131 | } 132 | .btn-danger:active, 133 | .btn-danger:focus, 134 | .btn-danger:hover, 135 | .btn-danger.active, 136 | .btn-danger.focus, 137 | .btn-danger:active, 138 | .btn-danger:focus, 139 | .btn-danger:hover, 140 | .open > .dropdown-toggle.btn-danger { 141 | background-color: #ed3c39 !important; 142 | border: 1px solid #ed3c39 !important; 143 | } 144 | .btn-inverse { 145 | background-color: #4c5667 !important; 146 | border: 1px solid #4c5667 !important; 147 | } 148 | .btn-inverse:hover, 149 | .btn-inverse:focus, 150 | .btn-inverse:active, 151 | .btn-inverse.active, 152 | .btn-inverse.focus, 153 | .btn-inverse:active, 154 | .btn-inverse:focus, 155 | .btn-inverse:hover, 156 | .open > .dropdown-toggle.btn-inverse { 157 | background-color: #414a58 !important; 158 | border: 1px solid #414a58 !important; 159 | } 160 | .btn-purple { 161 | background-color: #7266ba !important; 162 | border: 1px solid #7266ba !important; 163 | } 164 | .btn-purple:hover, 165 | .btn-purple:focus, 166 | .btn-purple:active { 167 | background-color: #6254b2 !important; 168 | border: 1px solid #6254b2 !important; 169 | } 170 | .btn-pink { 171 | background-color: #f76397 !important; 172 | border: 1px solid #f76397 !important; 173 | } 174 | .btn-pink:hover, 175 | .btn-pink:focus, 176 | .btn-pink:active { 177 | background-color: #f64b87 !important; 178 | border: 1px solid #f64b87 !important; 179 | } 180 | .btn-custom { 181 | box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4); 182 | -webkit-transition: all 0.4s; 183 | -o-transition: all 0.4s; 184 | transition: all 0.4s; 185 | } 186 | .btn-rounded { 187 | border-radius: 2em; 188 | padding: 6px 18px; 189 | } 190 | .fileupload { 191 | overflow: hidden; 192 | position: relative; 193 | } 194 | .fileupload input.upload { 195 | cursor: pointer; 196 | filter: alpha(opacity=0); 197 | font-size: 20px; 198 | margin: 0; 199 | opacity: 0; 200 | padding: 0; 201 | position: absolute; 202 | right: 0; 203 | top: 0; 204 | } 205 | -------------------------------------------------------------------------------- /app/controllers/campaigns_controller.rb: -------------------------------------------------------------------------------- 1 | class CampaignsController < ApplicationController 2 | before_action :authenticate_user!, except: [:home, :show] 3 | include CampaignsHelper 4 | 5 | def home 6 | # Retrieve all active campaigns 7 | @campaigns = Campaign.where(active: true).order(created_at: :desc).page params[:page] 8 | end 9 | 10 | def new 11 | # Redirect if no existing Stripe account 12 | unless current_user.stripe_account 13 | redirect_to new_stripe_account_path and return 14 | end 15 | 16 | # Populate random campaign info 17 | random_campaign 18 | 19 | # Create a new campaign object 20 | @campaign = Campaign.new 21 | end 22 | 23 | def create 24 | # Create a campaign for the user 25 | @campaign = current_user.campaigns.new(campaign_params) 26 | 27 | # Redirect to the campaign page once created 28 | if @campaign.save 29 | flash[:notice] = "Your campaign has been created!" 30 | redirect_to @campaign 31 | else 32 | handle_error(@campaign.errors.full_messages, 'new') 33 | end 34 | end 35 | 36 | def show 37 | # Retrieve a campaign 38 | @campaign = Campaign.find(params[:id]) 39 | 40 | # List all charges for a given campaign 41 | @charges = Charge.where(campaign_id: @campaign.id, amount_refunded: nil).order(created_at: :desc) 42 | end 43 | 44 | def dashboard 45 | # List campaigns for the current user 46 | @campaigns = current_user.campaigns.order(created_at: :desc) 47 | 48 | # Redirect if there's not a Stripe account for this user yet 49 | unless current_user.stripe_account 50 | flash[:success] = "Create an account to get started." 51 | redirect_to new_stripe_account_path and return 52 | end 53 | 54 | # Retrieve charges, transfers, balance transactions, & balance from Stripe 55 | begin 56 | @stripe_account = Stripe::Account.retrieve(current_user.stripe_account) 57 | 58 | # Last 100 charges 59 | @payments = Stripe::Charge.list( 60 | { 61 | limit: 100, 62 | expand: ['data.source_transfer.source_transaction.dispute', 'data.application_fee'], 63 | source: {object: 'all'} 64 | }, 65 | { stripe_account: current_user.stripe_account } 66 | ) 67 | 68 | # Last 100 payouts from the managed account to their bank account 69 | @payouts = Stripe::Payout.list( 70 | { 71 | limit: 100, 72 | expand: ['data.destination'] 73 | }, 74 | { stripe_account: current_user.stripe_account } 75 | ) 76 | 77 | # Retrieve available and pending balance for an account 78 | @balance = Stripe::Balance.retrieve(stripe_account: current_user.stripe_account) 79 | @balance_available = @balance.available.first.amount + @balance.pending.first.amount 80 | 81 | # Retrieve transactions with an available_on date in the future 82 | # For a large platform, it's generally preferrable to handle these async 83 | transactions = Stripe::BalanceTransaction.all( 84 | { 85 | limit: 100, 86 | available_on: {gte: Time.now.to_i} 87 | }, 88 | { stripe_account: current_user.stripe_account } 89 | ) 90 | 91 | # Iterate through transactions and sum values for each available_on date 92 | # For a production app, you'll probably want to store and query these locally instead 93 | balances = Hash.new 94 | transactions.auto_paging_each do |txn| 95 | if balances.key?(txn.available_on) 96 | balances[txn.available_on] += txn.net 97 | else 98 | balances[txn.available_on] = txn.net 99 | end 100 | end 101 | 102 | # Sort the results 103 | @transactions = balances.sort_by {|date,net| date} 104 | 105 | # Check for a debit card external account and determine amount for payout 106 | @debit_card = @stripe_account.external_accounts.find { |c| c.object == "card"} 107 | @instant_amt = @balance_available*0.97 108 | @instant_fee = @balance_available*0.03 109 | 110 | # Handle Stripe exceptions 111 | rescue Stripe::StripeError => e 112 | flash[:error] = e.message 113 | redirect_to root_path 114 | 115 | # Handle other exceptions 116 | rescue => e 117 | flash[:error] = e.message 118 | redirect_to root_path 119 | end 120 | end 121 | 122 | def edit 123 | # Retrieve the campaign 124 | @campaign = Campaign.find(params[:id]) 125 | end 126 | 127 | def update 128 | # Retrieve the campaign 129 | @campaign = Campaign.find(params[:id]) 130 | 131 | # Redirect to view campaign 132 | if @campaign.update_attributes(campaign_params) 133 | flash[:notice] = "Your campaign has been updated!" 134 | redirect_to @campaign 135 | else 136 | handle_error(@campaign.errors.full_messages, 'edit') 137 | end 138 | end 139 | 140 | def destroy 141 | # Retrieve the campaign 142 | campaign = Campaign.find(params[:id]) 143 | 144 | # Respond with deletion status 145 | if campaign.update_attributes(active: false) 146 | flash[:notice] = "Your campaign has been deleted." 147 | redirect_to dashboard_path 148 | else 149 | flash[:error] = "We weren't able to delete this campaign." 150 | redirect_to dashboard_path 151 | end 152 | end 153 | 154 | private 155 | def campaign_params 156 | params.require(:campaign).permit(:title, :description, :goal, :subscription, :image) 157 | end 158 | 159 | def random_campaign 160 | data = campaign_data.sample 161 | @campaign_title = data[:title] 162 | @campaign_description = data[:description] 163 | @campaign_image = data[:image] 164 | @goal = amounts.sample 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /app/assets/stylesheets/custom.scss: -------------------------------------------------------------------------------- 1 | @import "bootstrap-sprockets"; 2 | @import "bootstrap"; 3 | @import url(https://fonts.googleapis.com/css?family=Lato:400,300); 4 | html { 5 | position: relative; 6 | min-height: 100%; 7 | } 8 | body { 9 | padding-top: 60px; 10 | font-family: "Lato"; 11 | font-size: 16px; 12 | margin-bottom: 220px; 13 | background-color: #F6F9FC; 14 | } 15 | h1, h2, h3, h4 { 16 | font-family: "Lato"; 17 | font-weight: 300; 18 | } 19 | .footer { 20 | position: absolute; 21 | bottom: 0; 22 | width: 100%; 23 | height: 200px; 24 | padding-top: 40px; 25 | background-color: #272727; 26 | color: #fff; 27 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); 28 | a { 29 | color: #2494be; 30 | } 31 | a:hover { 32 | text-decoration: none; 33 | color: #fff; 34 | } 35 | } 36 | .lato { 37 | font-family: "Lato"; 38 | font-weight: 300; 39 | 40 | } 41 | .navbar-default { 42 | background-color: #fff; 43 | height: 60px; 44 | img { 45 | margin-top:10px; 46 | } 47 | .navbar-nav > .active > a, .navbar-nav > .active > a:hover, .navbar-nav > .active > a:focus { 48 | background-color: #fff; 49 | color: #000; 50 | } 51 | .navbar-collapse { 52 | background-color: #fff; 53 | } 54 | .fa { 55 | margin-right: 8px; 56 | } 57 | } 58 | .home-hero { 59 | background-image:url('https://unsplash.it/1200/600?image=0&blur'); 60 | background-size:cover; 61 | background-color: #666; 62 | color: #fff; 63 | height: 600px; 64 | h1 { 65 | font-size: 72px; 66 | } 67 | h3 { 68 | color: #f6f6f6; 69 | a { 70 | color: #fff; 71 | font-weight: bold; 72 | } 73 | } 74 | } 75 | .campaign { 76 | font-family: "Lato", Helvetica, Arial, sans-serif!important; 77 | font-weight: 300; 78 | transition: box-shadow .3s; 79 | h2 { 80 | margin-top: 0px; 81 | } 82 | a { 83 | color: #555; 84 | text-decoration: none; 85 | } 86 | a:hover { 87 | color: #7266ba; 88 | } 89 | img { 90 | width: 100%; 91 | } 92 | .panel-body { 93 | padding: 0px; 94 | } 95 | } 96 | .campaign:hover { 97 | webkit-box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1),0 5px 15px rgba(0, 0, 0, 0.07); 98 | -moz-box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1),0 5px 15px rgba(0, 0, 0, 0.07); 99 | box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1),0 5px 15px rgba(0, 0, 0, 0.07); 100 | } 101 | .campaign-content { 102 | img { 103 | width: 100%; 104 | webkit-box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1),0 5px 15px rgba(0, 0, 0, 0.07); 105 | -moz-box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1),0 5px 15px rgba(0, 0, 0, 0.07); 106 | box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1),0 5px 15px rgba(0, 0, 0, 0.07); 107 | } 108 | } 109 | .img-selected { 110 | border: 1px solid #0080FF; 111 | } 112 | .raised { 113 | color: #00b19d; 114 | } 115 | .feature { 116 | font-family: "Lato"; 117 | font-weight: 300; 118 | font-size: 26px; 119 | h2 { 120 | color: #4a4a4a; 121 | font-family: "Lato"; 122 | font-weight: 300; 123 | } 124 | p { 125 | font-size: 24px; 126 | font-weight: 300; 127 | color: #999; 128 | font-family: sans-serif; 129 | } 130 | .price { 131 | font-size: 200px 132 | } 133 | .price-sm { 134 | font-size: 100px; 135 | } 136 | } 137 | .icon-lg { 138 | font-size: 100px; 139 | } 140 | .icon-huge { 141 | font-size:200px; 142 | } 143 | .topspace { 144 | margin-top: 10px; 145 | } 146 | .topspace-lg { 147 | margin-top: 50px; 148 | } 149 | .leftspace { 150 | margin-left: 8px; 151 | } 152 | .body-padding { 153 | padding: 15px; 154 | } 155 | .red { 156 | color: #B20000; 157 | } 158 | .gray { 159 | color: #666; 160 | } 161 | .pink { 162 | color: #D80A84; 163 | } 164 | .blue { 165 | color: #0080FF; 166 | } 167 | .purple { 168 | color: #7266ba; 169 | } 170 | .white { 171 | color: #fff; 172 | } 173 | .shadow-tiny { 174 | webkit-box-shadow: 0 2px 0 rgba(37,46,53,0.06); 175 | -moz-box-shadow: 0 2px 0 rgba(37,46,53,0.06); 176 | box-shadow: 0 2px 0 rgba(37,46,53,0.06); 177 | } 178 | .shadow-sm { 179 | webkit-box-shadow: 2px 2px 2px rgba(37,46,53,0.06); 180 | -moz-box-shadow: 2px 2px 2px rgba(37,46,53,0.06); 181 | box-shadow: 2px 2px 2px rgba(37,46,53,0.06); 182 | } 183 | .field_with_errors { 184 | @extend .has-error !optional; 185 | display: inline; 186 | } 187 | .highlight { 188 | background-color: #CCE6FF; 189 | } 190 | /* flash */ 191 | .alert-alert { 192 | background-color: #ef5350; 193 | border-color: #eed3d7; 194 | color: #fff; 195 | text-align: left; 196 | } 197 | .alert-success { 198 | background-color: #00b19d; 199 | border-color: #d6e9c6; 200 | color: #fff; 201 | text-align: left; 202 | } 203 | .alert-notice { 204 | background-color: #00b19d; 205 | border-color: #d6e9c6; 206 | color: #fff; 207 | text-align: left; 208 | } 209 | .alert-danger h2, .alert-error h2 { 210 | margin-top:0px; 211 | } 212 | .box-shadow { 213 | -webkit-box-shadow: 0px 2px 3px 0px rgba(50, 50, 50, 0.75); 214 | -moz-box-shadow: 0px 2px 3px 0px rgba(50, 50, 50, 0.75); 215 | box-shadow: 0px 2px 3px 0px rgba(50, 50, 50, 0.75); 216 | } 217 | .btn-xl { 218 | font-size: 30px; 219 | padding: 15px; 220 | } 221 | .placeholder label { 222 | display: none !important; 223 | } 224 | .form-control:focus, .StripeElement--focus { 225 | border-color: #939393; 226 | box-shadow: none; 227 | } 228 | .StripeElement--invalid { 229 | border-color: #a94442; 230 | box-shadow: none; 231 | } 232 | .form-control, .has-error .form-control:focus { 233 | border-width: 1px; 234 | box-shadow: none; 235 | } 236 | .panel-payment { 237 | webkit-box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1),0 5px 15px rgba(0, 0, 0, 0.07); 238 | -moz-box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1),0 5px 15px rgba(0, 0, 0, 0.07); 239 | box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1),0 5px 15px rgba(0, 0, 0, 0.07); 240 | ::-webkit-input-placeholder, input { 241 | font-size: 15px!important; 242 | color: #666; 243 | font-family: "Open Sans", sans-serif; 244 | -webkit-font-smoothing: antialiased; 245 | -moz-osx-font-smoothing: grayscale; 246 | } 247 | input { 248 | color: #000; 249 | } 250 | } 251 | #card-errors { 252 | display: none; 253 | } 254 | 255 | /* media queries */ 256 | @media only screen 257 | and (min-device-width : 375px) 258 | and (max-device-width : 667px) { 259 | .home-hero h1 { 260 | font-size: 60px; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.0.7) 5 | actionpack (= 5.0.7) 6 | nio4r (>= 1.2, < 3.0) 7 | websocket-driver (~> 0.6.1) 8 | actionmailer (5.0.7) 9 | actionpack (= 5.0.7) 10 | actionview (= 5.0.7) 11 | activejob (= 5.0.7) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.0.7) 15 | actionview (= 5.0.7) 16 | activesupport (= 5.0.7) 17 | rack (~> 2.0) 18 | rack-test (~> 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.0.7) 22 | activesupport (= 5.0.7) 23 | builder (~> 3.1) 24 | erubis (~> 2.7.0) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.0.7) 28 | activesupport (= 5.0.7) 29 | globalid (>= 0.3.6) 30 | activemodel (5.0.7) 31 | activesupport (= 5.0.7) 32 | activerecord (5.0.7) 33 | activemodel (= 5.0.7) 34 | activesupport (= 5.0.7) 35 | arel (~> 7.0) 36 | activesupport (5.0.7) 37 | concurrent-ruby (~> 1.0, >= 1.0.2) 38 | i18n (>= 0.7, < 2) 39 | minitest (~> 5.1) 40 | tzinfo (~> 1.1) 41 | arel (7.1.4) 42 | autoprefixer-rails (8.5.0) 43 | execjs 44 | bcrypt (3.1.12) 45 | bindex (0.5.0) 46 | bootstrap-sass (3.3.7) 47 | autoprefixer-rails (>= 5.2.1) 48 | sass (>= 3.3.4) 49 | builder (3.2.3) 50 | byebug (10.0.2) 51 | coffee-rails (4.2.2) 52 | coffee-script (>= 2.2.0) 53 | railties (>= 4.0.0) 54 | coffee-script (2.4.1) 55 | coffee-script-source 56 | execjs 57 | coffee-script-source (1.12.2) 58 | concurrent-ruby (1.0.5) 59 | crass (1.0.4) 60 | data-confirm-modal (1.6.2) 61 | railties (>= 3.0) 62 | devise (4.4.3) 63 | bcrypt (~> 3.0) 64 | orm_adapter (~> 0.1) 65 | railties (>= 4.1.0, < 6.0) 66 | responders 67 | warden (~> 1.2.3) 68 | domain_name (0.5.20180417) 69 | unf (>= 0.0.5, < 1.0.0) 70 | erubis (2.7.0) 71 | execjs (2.7.0) 72 | faraday (0.15.1) 73 | multipart-post (>= 1.2, < 3) 74 | ffi (1.9.23) 75 | font-awesome-rails (4.7.0.4) 76 | railties (>= 3.2, < 6.0) 77 | globalid (0.4.1) 78 | activesupport (>= 4.2.0) 79 | http-cookie (1.0.3) 80 | domain_name (~> 0.5) 81 | i18n (1.0.1) 82 | concurrent-ruby (~> 1.0) 83 | jbuilder (2.7.0) 84 | activesupport (>= 4.2.0) 85 | multi_json (>= 1.2) 86 | jquery-rails (4.3.3) 87 | rails-dom-testing (>= 1, < 3) 88 | railties (>= 4.2.0) 89 | thor (>= 0.14, < 2.0) 90 | kaminari (1.1.1) 91 | activesupport (>= 4.1.0) 92 | kaminari-actionview (= 1.1.1) 93 | kaminari-activerecord (= 1.1.1) 94 | kaminari-core (= 1.1.1) 95 | kaminari-actionview (1.1.1) 96 | actionview 97 | kaminari-core (= 1.1.1) 98 | kaminari-activerecord (1.1.1) 99 | activerecord 100 | kaminari-core (= 1.1.1) 101 | kaminari-core (1.1.1) 102 | listen (3.0.8) 103 | rb-fsevent (~> 0.9, >= 0.9.4) 104 | rb-inotify (~> 0.9, >= 0.9.7) 105 | loofah (2.2.2) 106 | crass (~> 1.0.2) 107 | nokogiri (>= 1.5.9) 108 | mail (2.7.0) 109 | mini_mime (>= 0.1.1) 110 | mailgun-ruby (1.1.9) 111 | rest-client (~> 2.0) 112 | method_source (0.9.0) 113 | mime-types (3.1) 114 | mime-types-data (~> 3.2015) 115 | mime-types-data (3.2016.0521) 116 | mini_mime (1.0.0) 117 | mini_portile2 (2.3.0) 118 | minitest (5.11.3) 119 | multi_json (1.13.1) 120 | multipart-post (2.0.0) 121 | netrc (0.11.0) 122 | nio4r (2.3.1) 123 | nokogiri (1.8.2) 124 | mini_portile2 (~> 2.3.0) 125 | orm_adapter (0.5.0) 126 | pg (0.18.4) 127 | puma (3.11.4) 128 | rack (2.0.5) 129 | rack-test (0.6.3) 130 | rack (>= 1.0) 131 | rails (5.0.7) 132 | actioncable (= 5.0.7) 133 | actionmailer (= 5.0.7) 134 | actionpack (= 5.0.7) 135 | actionview (= 5.0.7) 136 | activejob (= 5.0.7) 137 | activemodel (= 5.0.7) 138 | activerecord (= 5.0.7) 139 | activesupport (= 5.0.7) 140 | bundler (>= 1.3.0) 141 | railties (= 5.0.7) 142 | sprockets-rails (>= 2.0.0) 143 | rails-dom-testing (2.0.3) 144 | activesupport (>= 4.2.0) 145 | nokogiri (>= 1.6) 146 | rails-html-sanitizer (1.0.4) 147 | loofah (~> 2.2, >= 2.2.2) 148 | railties (5.0.7) 149 | actionpack (= 5.0.7) 150 | activesupport (= 5.0.7) 151 | method_source 152 | rake (>= 0.8.7) 153 | thor (>= 0.18.1, < 2.0) 154 | rake (12.3.1) 155 | rb-fsevent (0.10.3) 156 | rb-inotify (0.9.10) 157 | ffi (>= 0.5.0, < 2) 158 | responders (2.4.0) 159 | actionpack (>= 4.2.0, < 5.3) 160 | railties (>= 4.2.0, < 5.3) 161 | rest-client (2.0.2) 162 | http-cookie (>= 1.0.2, < 2.0) 163 | mime-types (>= 1.16, < 4.0) 164 | netrc (~> 0.8) 165 | sass (3.5.6) 166 | sass-listen (~> 4.0.0) 167 | sass-listen (4.0.0) 168 | rb-fsevent (~> 0.9, >= 0.9.4) 169 | rb-inotify (~> 0.9, >= 0.9.7) 170 | sass-rails (5.0.7) 171 | railties (>= 4.0.0, < 6) 172 | sass (~> 3.1) 173 | sprockets (>= 2.8, < 4.0) 174 | sprockets-rails (>= 2.0, < 4.0) 175 | tilt (>= 1.1, < 3) 176 | spring (2.0.2) 177 | activesupport (>= 4.2) 178 | spring-watcher-listen (2.0.1) 179 | listen (>= 2.7, < 4.0) 180 | spring (>= 1.2, < 3.0) 181 | sprockets (3.7.1) 182 | concurrent-ruby (~> 1.0) 183 | rack (> 1, < 3) 184 | sprockets-rails (3.2.1) 185 | actionpack (>= 4.0) 186 | activesupport (>= 4.0) 187 | sprockets (>= 3.0.0) 188 | sqlite3 (1.3.13) 189 | stripe (3.15.0) 190 | faraday (~> 0.10) 191 | thor (0.20.0) 192 | thread_safe (0.3.6) 193 | tilt (2.0.8) 194 | tzinfo (1.2.5) 195 | thread_safe (~> 0.1) 196 | uglifier (4.1.10) 197 | execjs (>= 0.3.0, < 3) 198 | unf (0.1.4) 199 | unf_ext 200 | unf_ext (0.0.7.5) 201 | warden (1.2.7) 202 | rack (>= 1.0) 203 | web-console (3.6.2) 204 | actionview (>= 5.0) 205 | activemodel (>= 5.0) 206 | bindex (>= 0.4.0) 207 | railties (>= 5.0) 208 | websocket-driver (0.6.5) 209 | websocket-extensions (>= 0.1.0) 210 | websocket-extensions (0.1.3) 211 | 212 | PLATFORMS 213 | ruby 214 | 215 | DEPENDENCIES 216 | bootstrap-sass (~> 3.3.6) 217 | byebug 218 | coffee-rails (~> 4.2) 219 | data-confirm-modal 220 | devise (~> 4.2, >= 4.2.1) 221 | font-awesome-rails 222 | jbuilder (~> 2.5) 223 | jquery-rails 224 | kaminari 225 | listen (~> 3.0.5) 226 | mailgun-ruby 227 | pg (~> 0.18.4) 228 | puma (~> 3.0) 229 | rails (~> 5.0.0, >= 5.0.0.1) 230 | sass-rails (~> 5.0) 231 | spring 232 | spring-watcher-listen (~> 2.0.0) 233 | sqlite3 234 | stripe 235 | tzinfo-data 236 | uglifier (>= 1.3.0) 237 | web-console 238 | 239 | BUNDLED WITH 240 | 1.16.1 241 | -------------------------------------------------------------------------------- /app/controllers/stripe_accounts_controller.rb: -------------------------------------------------------------------------------- 1 | class StripeAccountsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def new 5 | @account = StripeAccount.new 6 | end 7 | 8 | def create 9 | @account = StripeAccount.new(account_params) 10 | 11 | if @account.save 12 | begin 13 | # There are different requirements for individuals vs companies 14 | # Using separate request for each in this example 15 | 16 | # If this is an individual, send request params to create an individual acct 17 | if account_params[:account_type].eql?('individual') 18 | stripe_account = Stripe::Account.create( 19 | type: 'custom', 20 | requested_capabilities: ['platform_payments'], # Donors interact with the platform 21 | business_type: 'individual', 22 | individual: { 23 | first_name: account_params[:first_name].capitalize, 24 | last_name: account_params[:last_name].capitalize, 25 | dob: { 26 | day: account_params[:dob_day], 27 | month: account_params[:dob_month], 28 | year: account_params[:dob_year], 29 | }, 30 | ssn_last_4: account_params[:ssn_last_4], 31 | }, 32 | business_profile: { 33 | product_description: 'Fundraising campaign', 34 | }, 35 | tos_acceptance: { 36 | date: Time.now.to_i, 37 | ip: request.remote_ip 38 | } 39 | ) 40 | 41 | # If the account type is a company, send company information 42 | else 43 | stripe_account = Stripe::Account.create( 44 | type: 'custom', 45 | requested_capabilities: ['platform_payments'], # Donors interact with the platform 46 | business_type: 'company', 47 | company: { 48 | name: account_params[:business_name], 49 | tax_id: account_params[:business_tax_id], 50 | }, 51 | business_profile: { 52 | product_description: 'Fundraising campaign', 53 | }, 54 | tos_acceptance: { 55 | date: Time.now.to_i, 56 | ip: request.remote_ip 57 | } 58 | ) 59 | end 60 | 61 | # Save the account ID for this user for later 62 | @account.acct_id = stripe_account.id 63 | @account.save 64 | current_user.stripe_account = stripe_account.id 65 | 66 | if current_user.save 67 | flash[:success] = "Your account has been created! 68 | Next, add a bank account where you'd like to receive transfers below." 69 | redirect_to new_bank_account_path 70 | else 71 | handle_error("Sorry, we weren't able to create this account.", 'new') 72 | end 73 | 74 | # Handle exceptions from Stripe 75 | rescue Stripe::StripeError => e 76 | handle_error(e.message, 'new') 77 | 78 | # Handle any other exceptions 79 | rescue => e 80 | handle_error(e.message, 'new') 81 | end 82 | else 83 | @full_account = true if params[:full_account] 84 | handle_error(@account.errors.full_messages) 85 | end 86 | end 87 | 88 | def edit 89 | # Check for a valid account ID 90 | unless params[:id] && params[:id].eql?(current_user.stripe_account) 91 | flash[:error] = "No Stripe account specified" 92 | redirect_to dashboard_path and return 93 | end 94 | 95 | # Retrieve the Stripe account to find fields needed 96 | @stripe_account = Stripe::Account.retrieve(params[:id]) 97 | 98 | # Retrieve the local account details 99 | @account = StripeAccount.find_by(acct_id: params[:id]) 100 | 101 | if @stripe_account.requirements.currently_due.empty? 102 | flash[:success] = "Your information is all up to date." 103 | redirect_to dashboard_path and return 104 | end 105 | end 106 | 107 | def update 108 | # Check for an existing Stripe account 109 | unless current_user.stripe_account 110 | redirect_to new_stripe_account_path and return 111 | end 112 | 113 | begin 114 | # Retrieve the Stripe account 115 | @stripe_account = Stripe::Account.retrieve(current_user.stripe_account) 116 | 117 | @account = StripeAccount.new(account_params) 118 | 119 | 120 | # Reject empty values 121 | account_params.each do |key, value| 122 | if value.empty? 123 | flash.now[:alert] = "Please complete all fields." 124 | render 'edit' and return 125 | end 126 | end 127 | 128 | # Iterate through each field needed 129 | @stripe_account.requirements.eventually_due.each do |field| 130 | 131 | # Update each needed attribute 132 | case field 133 | when 'individual.address.city' 134 | @stripe_account.individual.address.city = account_params[:address_city] 135 | when 'individual.address.line1' 136 | @stripe_account.individual.address.line1 = account_params[:address_line1] 137 | when 'individual.address.postal_code' 138 | @stripe_account.individual.address.postal_code = account_params[:address_postal] 139 | when 'individual.address.state' 140 | @stripe_account.individual.address.state = account_params[:address_state] 141 | when 'individual.dob.day' 142 | @stripe_account.individual.dob.day = account_params[:dob_day] 143 | when 'individual.dob.month' 144 | @stripe_account.individual.dob.month = account_params[:dob_month] 145 | when 'individual.dob.year' 146 | @stripe_account.individual.dob.year = account_params[:dob_year] 147 | when 'individual.first_name' 148 | @stripe_account.individual.first_name = account_params[:first_name] 149 | when 'individual.last_name' 150 | @stripe_account.individual.last_name = account_params[:last_name] 151 | when 'individual.ssn_last_4' 152 | @stripe_account.individual.ssn_last_4 = account_params[:ssn_last_4] 153 | when 'business_type' 154 | @stripe_account.business_type = account_params[:type] 155 | when 'individual.id_number' 156 | @stripe_account.individual.id_number = account_params[:personal_id_number] 157 | when 'individual.verification.document' 158 | @stripe_account.individual.verification.document.front = account_params[:verification_document] 159 | when 'company.name' 160 | @stripe_account.company.name = account_params[:business_name] 161 | when 'company.tax_id' 162 | @stripe_account.company.tax_id = account_params[:business_tax_id] 163 | end 164 | end 165 | 166 | @stripe_account.save 167 | flash[:success] = "Thanks! Your account has been updated." 168 | redirect_to dashboard_path and return 169 | 170 | # Handle exceptions from Stripe 171 | rescue Stripe::StripeError => e 172 | handle_error(e.message, 'edit') 173 | 174 | # Handle any other exceptions 175 | rescue => e 176 | handle_error(e.message, 'edit') 177 | end 178 | end 179 | 180 | private 181 | def account_params 182 | params.require(:stripe_account).permit( 183 | :first_name, :last_name, :account_type, :dob_month, :dob_day, :dob_year, :tos, 184 | :ssn_last_4, :address_line1, :address_city, :address_state, :address_postal, :business_name, 185 | :business_tax_id, :full_account, :personal_id_number, :verification_document 186 | ) 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /app/views/campaigns/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_for :page_title, "#{@campaign.title.titleize} | Fundraising Marketplace" %> 2 | <% content_for(:header) do %> 3 | <% if params[:checkout].present? %> 4 | 5 | 49 | <% else %> 50 | 51 | 142 | <% end %> 143 | <% end %> 144 |
    145 |
    146 |
    147 | <%= render 'layouts/messages' %> 148 | <% unless @campaign.active %> 149 |
    150 | This campaign is no longer active. 151 |
    152 | <% end %> 153 |
    154 | <%= image_tag("#{@campaign.image}_lg.jpg", class: "img-responsive") %> 155 |
    156 | 159 |

    160 | <% if @campaign.raised %> 161 | <%= format_amount(@campaign.raised) %> raised so far of <%= number_to_currency(@campaign.goal, precision: 0) %> goal 162 | <% else %> 163 | <%= number_to_currency(@campaign.goal) %> goal 164 | <% end %> 165 |

    166 |
    167 | <%= simple_format(@campaign.description) %> 168 |
    169 | 170 | <% unless @campaign.raised.to_i > 0 %> 171 | This campaign has not received any donations yet. 172 | <% else %> 173 | <%= render 'donations' %> 174 | <% end %> 175 |
    176 |
    177 | <% if @campaign.active %> 178 | <% if params[:checkout].present? %> 179 | <%= render 'checkoutform' %> 180 | <% else %> 181 | <%= render 'paymentform' %> 182 | <% end %> 183 |
    184 |

    185 | Hint: use a Stripe test card and any valid expiry. 186 |

    187 |
    188 | <% end %> 189 | <% if current_user && @campaign.user_id.eql?(current_user.id) && @campaign.active %> 190 | 191 |

    192 | <%= link_to "Edit this campaign", edit_campaign_path(@campaign), 193 | class: "btn btn-lg btn-primary btn-custom btn-block"%> 194 |

    195 |

    196 | <%= link_to "Delete this campaign", campaign_path(@campaign), method: :delete, data: { confirm: "Are you sure you want to delete this campaign?" }, class: "btn btn-lg btn-block btn-custom btn-danger" %> 197 |

    198 | <% end %> 199 |
    200 |
    201 |
    202 | --------------------------------------------------------------------------------