├── log └── .keep ├── tmp └── .keep ├── vendor ├── .keep └── assets │ └── javascripts │ └── active_admin.js ├── lib ├── assets │ └── .keep ├── tasks │ ├── .keep │ ├── migrate_discount_to_partials.rake │ ├── plan_creation.rake │ └── consistency_test.rake └── templates │ └── slim │ └── scaffold │ └── _form.html.slim ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── app ├── assets │ ├── images │ │ ├── .keep │ │ └── creditcards │ │ │ ├── amex.png │ │ │ ├── jcb.png │ │ │ ├── solo.png │ │ │ ├── visa.png │ │ │ ├── credit.png │ │ │ ├── forbru.png │ │ │ ├── google.png │ │ │ ├── laser.png │ │ │ ├── money.png │ │ │ ├── paypal.png │ │ │ ├── dankort.png │ │ │ ├── discover.png │ │ │ ├── maestro.png │ │ │ ├── shopify.png │ │ │ ├── dinersclub.png │ │ │ └── mastercard.png │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── application.js │ │ ├── cable.js │ │ └── edits_affiliate.es6 │ ├── stylesheets │ │ ├── application.scss │ │ └── active_admin.scss │ └── config │ │ └── manifest.js ├── models │ ├── concerns │ │ ├── .keep │ │ └── has_reference.rb │ ├── application_record.rb │ ├── null_discount_code.rb │ ├── event.rb │ ├── address.rb │ ├── null_day_revenue.rb │ ├── performance.rb │ ├── subscription_cart.rb │ ├── plan.rb │ ├── affiliate.rb │ ├── user.rb │ ├── ticket.rb │ ├── discount_code.rb │ ├── subscription.rb │ ├── day_revenue.rb │ ├── stripe_customer.rb │ ├── stripe_token.rb │ ├── stripe_refund.rb │ ├── payment_line_item.rb │ ├── stripe_charge.rb │ ├── shopping_cart.rb │ └── stripe_account.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── visitors_controller.rb │ ├── plans_controller.rb │ ├── events_controller.rb │ ├── discount_codes_controller.rb │ ├── pay_pal_payments_controller.rb │ ├── addresses_controller.rb │ ├── subscription_carts_controller.rb │ ├── users_controller.rb │ ├── shopping_carts_controller.rb │ ├── user_simulations_controller.rb │ ├── daily_revenue_reports_controller.rb │ ├── stripe_webhook_controller.rb │ ├── affiliates_controller.rb │ ├── refunds_controller.rb │ ├── subscriptions_controller.rb │ ├── application_controller.rb │ └── users │ │ └── sessions_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.slim │ │ ├── mailer.html.slim │ │ ├── _navigation.html.slim │ │ ├── _messages.html.slim │ │ ├── _navigation_links.html.slim │ │ └── application.html.slim │ ├── visitors │ │ └── index.html.slim │ ├── events │ │ ├── index.html.slim │ │ └── show.html.slim │ ├── daily_revenue_reports │ │ └── show.html.slim │ ├── users │ │ ├── sessions │ │ │ └── two_factor.html.slim │ │ └── show.html.slim │ ├── subscriptions │ │ └── edit.html.slim │ ├── plans │ │ └── index.html.slim │ ├── subscription_carts │ │ └── show.html.slim │ ├── affiliates │ │ ├── edit.html.slim │ │ ├── edit │ │ │ └── _bank_account_form.html.slim │ │ └── new.html.slim │ ├── payments │ │ └── show.html.slim │ ├── shopping_carts │ │ └── show.html.slim │ └── addresses │ │ └── new.html.slim ├── jobs │ ├── application_job.rb │ ├── notify_tax_cloud_job.rb │ ├── notify_tax_cloud_of_refund_job.rb │ ├── refund_charge_job.rb │ ├── executes_stripe_payment_job.rb │ ├── build_day_revenue_job.rb │ └── prepares_cart_for_stripe_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── mailers │ ├── application_mailer.rb │ ├── refund_mailer.rb │ └── payment_mailer.rb ├── helpers │ └── application_helper.rb ├── exceptions │ ├── pre_existing_payment_exception.rb │ ├── unathorized_purchase_exception.rb │ ├── tax_validity_exception.rb │ └── charge_setup_validity_exception.rb ├── policies │ ├── payment_policy.rb │ ├── payment_line_item_policy.rb │ ├── user_policy.rb │ └── application_policy.rb ├── workflows │ ├── stripe_handler │ │ ├── null_handler.rb │ │ ├── customer_subscription_deleted.rb │ │ ├── account_updated.rb │ │ └── invoice_payment_succeeded.rb │ ├── adds_plan_to_cart.rb │ ├── notifies_tax_cloud_of_refund.rb │ ├── adds_discount_code_to_cart.rb │ ├── adds_to_cart.rb │ ├── cash_purchases_cart.rb │ ├── adds_affiliate_to_cart.rb │ ├── adds_shipping_to_cart.rb │ ├── notifies_tax_cloud.rb │ ├── prepares_cart_for_pay_pal.rb │ ├── updates_affiliate_account.rb │ ├── adds_affiliate_account.rb │ ├── cancels_stripe_subscription.rb │ ├── prepares_cart_for_stripe.rb │ ├── creates_plan.rb │ ├── executes_stripe_payment.rb │ ├── creates_subscription_via_stripe.rb │ ├── changes_stripe_subscription_plan.rb │ ├── executes_pay_pal_payment.rb │ ├── prepares_stripe_refund.rb │ └── creates_stripe_refund.rb ├── admin │ ├── discount_codes.rb │ ├── plans.rb │ ├── performances.rb │ ├── payment_line_items.rb │ ├── events.rb │ ├── dashboard.rb │ ├── tickets.rb │ ├── subscriptions.rb │ ├── payments.rb │ └── users.rb ├── reports │ ├── day_revenue_report.rb │ ├── purchase_audit.rb │ ├── reportable.rb │ └── daily_revenue.rb └── services │ └── price_calculator.rb ├── .rspec ├── config ├── initializers │ ├── paper_trail.rb │ ├── delayed_job_config.rb │ ├── paypal.rb │ ├── authy.rb │ ├── tax_cloud.rb │ ├── mime_types.rb │ ├── stripe.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── backtrace_silencers.rb │ ├── mail_interceptor.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ └── inflections.rb ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── routes.rb ├── paypal.yml ├── locales │ ├── simple_form.en.yml │ └── en.yml ├── application.rb ├── secrets.yml └── environments │ ├── test.rb │ └── development.rb ├── package.json ├── spec ├── models │ ├── event_spec.rb │ ├── discount_code_spec.rb │ ├── subscription_spec.rb │ ├── payment_line_item_spec.rb │ ├── user_spec.rb │ ├── stripe_token_spec.rb │ ├── plan_spec.rb │ ├── performance_spec.rb │ ├── ticket_spec.rb │ ├── payment_spec.rb │ ├── affiliate_spec.rb │ ├── stripe_account_spec.rb │ ├── day_revenue_spec.rb │ └── shopping_cart_spec.rb ├── support │ ├── factory_bot.rb │ ├── devise.rb │ ├── paper_trail.rb │ ├── helpers.rb │ ├── capybara.rb │ ├── vcr.rb │ ├── database_cleaner.rb │ └── helpers │ │ └── session_helpers.rb ├── mailers │ ├── payment_mailer_spec.rb │ └── previews │ │ └── payment_mailer_preview.rb ├── factories │ ├── events.rb │ ├── users.rb │ ├── payment_line_items.rb │ ├── day_revenues.rb │ ├── shopping_carts.rb │ ├── tickets.rb │ ├── performances.rb │ ├── addresses.rb │ ├── affiliates.rb │ ├── discount_codes.rb │ ├── payments.rb │ ├── subscriptions.rb │ └── plans.rb ├── features │ ├── visitors │ │ ├── home_page_spec.rb │ │ └── sign_up_spec.rb │ ├── users │ │ ├── user_delete_spec.rb │ │ ├── sign_out_spec.rb │ │ ├── user_edit_spec.rb │ │ └── sign_in_spec.rb │ ├── subscriptions │ │ ├── user_adds_plan_to_cart_spec.rb │ │ ├── user_purchases_subscription_spec.rb │ │ ├── user_cancels_subscription_spec.rb │ │ └── user_changes_a_subscription_plan_spec.rb │ └── shopping_cart │ │ └── adding_to_cart_spec.rb ├── workflows │ ├── adds_affiliate_account_spec.rb │ ├── adds_plan_to_cart_spec.rb │ ├── adds_shipping_to_cart_spec.rb │ ├── creates_plan_spec.rb │ ├── creates_subscription_via_stripe_spec.rb │ ├── cancels_stripe_subscription_spec.rb │ ├── changes_stripe_subscription_plan_spec.rb │ ├── adds_to_cart_spec.rb │ ├── executes_pay_pal_payment_spec.rb │ ├── adds_affiliate_to_cart_spec.rb │ ├── adds_discount_code_to_cart_spec.rb │ └── creates_stripe_refund_spec.rb ├── controllers │ ├── users_controller_spec.rb │ └── shopping_carts_controller_spec.rb ├── reports │ ├── day_revenue_report_spec.rb │ └── report_builder_spec.rb ├── jobs │ └── build_day_revenue_job_spec.rb └── services │ └── tax_calculator_spec.rb ├── bin ├── bundle ├── delayed_job ├── rake ├── rails ├── yarn ├── spring ├── update └── setup ├── config.ru ├── db ├── migrate │ ├── 20180207163810_add_name_to_users.rb │ ├── 20180326183937_add_role_to_users.rb │ ├── 20180310154635_add_stripe_customer_to_user.rb │ ├── 20180307143259_add_reference_to_ticket.rb │ ├── 20180409123243_add_authy_to_user.rb │ ├── 20180416115829_add_partial_prices_to_payment.rb │ ├── 20180416140942_add_shipping_method_to_purchase.rb │ ├── 20180208122215_create_events.rb │ ├── 20180208122518_create_performances.rb │ ├── 20180415091739_create_day_revenues.rb │ ├── 20180217152755_create_payment_line_items.rb │ ├── 20180208123252_create_tickets.rb │ ├── 20180422155758_more_affiliate_fields.rb │ ├── 20180416140455_create_shopping_carts.rb │ ├── 20180310161419_create_subscriptions.rb │ ├── 20180310143147_create_plans.rb │ ├── 20180217152612_create_payments.rb │ ├── 20180416135846_create_addresses.rb │ ├── 20180327143530_refund_columns.rb │ ├── 20180325143544_create_active_admin_comments.rb │ ├── 20180330084649_create_discount_codes.rb │ ├── 20180409081144_add_object_changes_to_versions.rb │ ├── 20180420165023_create_affiliates.rb │ ├── 20180307134551_create_delayed_jobs.rb │ ├── 20180207163454_devise_create_users.rb │ └── 20180409081143_create_versions.rb └── seeds.rb ├── Rakefile ├── .gitignore ├── README.md └── .circleci └── config.yml /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.slim: -------------------------------------------------------------------------------- 1 | = yield 2 | -------------------------------------------------------------------------------- /app/views/visitors/index.html.slim: -------------------------------------------------------------------------------- 1 | h3 Welcome -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.slim: -------------------------------------------------------------------------------- 1 | html 2 | body 3 | = yield 4 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/active_admin.js: -------------------------------------------------------------------------------- 1 | //= require active_admin/base 2 | -------------------------------------------------------------------------------- /config/initializers/paper_trail.rb: -------------------------------------------------------------------------------- 1 | PaperTrail.config.track_associations = false 2 | -------------------------------------------------------------------------------- /config/initializers/delayed_job_config.rb: -------------------------------------------------------------------------------- 1 | Delayed::Worker.destroy_failed_jobs = false -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | @import "bootstrap-sprockets"; 2 | @import "bootstrap"; -------------------------------------------------------------------------------- /app/controllers/visitors_controller.rb: -------------------------------------------------------------------------------- 1 | class VisitorsController < ApplicationController 2 | end -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snow_globe", 3 | "private": true, 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /spec/models/event_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Event, type: :model do 4 | end 5 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryBot::Syntax::Methods 3 | end -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /config/initializers/paypal.rb: -------------------------------------------------------------------------------- 1 | PayPal::SDK.load("config/paypal.yml", Rails.env) 2 | PayPal::SDK.logger = Rails.logger 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /spec/models/discount_code_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe DiscountCode, type: :model do 4 | end 5 | -------------------------------------------------------------------------------- /spec/models/subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Subscription, type: :model do 4 | end 5 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | 3 | include Rollbar::ActiveJob 4 | 5 | end 6 | -------------------------------------------------------------------------------- /app/models/null_discount_code.rb: -------------------------------------------------------------------------------- 1 | class NullDiscountCode < DiscountCode 2 | 3 | def percentage 4 | 0 5 | end 6 | end -------------------------------------------------------------------------------- /spec/mailers/payment_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe PaymentMailer, type: :mailer do 4 | end 5 | -------------------------------------------------------------------------------- /spec/models/payment_line_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe PaymentLineItem, type: :model do 4 | end 5 | -------------------------------------------------------------------------------- /spec/support/devise.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include Devise::Test::ControllerHelpers, type: :controller 3 | end -------------------------------------------------------------------------------- /app/assets/images/creditcards/amex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/amex.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/jcb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/jcb.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/solo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/solo.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/visa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/visa.png -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/support/paper_trail.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:each) do 3 | PaperTrail.enabled = false 4 | end 5 | end -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/creditcards/credit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/credit.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/forbru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/forbru.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/google.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/laser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/laser.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/money.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/money.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/paypal.png -------------------------------------------------------------------------------- /app/views/events/index.html.slim: -------------------------------------------------------------------------------- 1 | h2 Upcoming Performances 2 | 3 | - @events.each do |event| 4 | h2= link_to(event.name, event_path(event)) -------------------------------------------------------------------------------- /app/assets/images/creditcards/dankort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/dankort.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/discover.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/maestro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/maestro.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/shopify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/shopify.png -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /config/initializers/authy.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | Authy.api_key = Rails.application.secrets.authy_key 4 | Authy.api_uri = "https://api.authy.com/" -------------------------------------------------------------------------------- /app/assets/images/creditcards/dinersclub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/dinersclub.png -------------------------------------------------------------------------------- /app/assets/images/creditcards/mastercard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-kh/take-my-money/HEAD/app/assets/images/creditcards/mastercard.png -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /app/views/daily_revenue_reports/show.html.slim: -------------------------------------------------------------------------------- 1 | h2 Daily Revenue Report 2 | 3 | = link_to "Download CSV", daily_revenue_report_path(format: :csv) 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/controllers/plans_controller.rb: -------------------------------------------------------------------------------- 1 | class PlansController < ApplicationController 2 | 3 | def index 4 | @plans = Plan.active.all 5 | end 6 | end -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/mailers/refund_mailer.rb: -------------------------------------------------------------------------------- 1 | class RefundMailer < ApplicationMailer 2 | 3 | def notify_success(order) 4 | end 5 | 6 | def notify_failure(order) 7 | end 8 | end -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/factories/events.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :event do 3 | name "Name" 4 | description "Description" 5 | image_url "URI" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user do 3 | name "User" 4 | email "test@example.com" 5 | password "password" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'support/helpers/session_helpers' 2 | 3 | RSpec.configure do |config| 4 | config.include Features::SessionHelpers, type: :feature 5 | end -------------------------------------------------------------------------------- /spec/factories/payment_line_items.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :payment_line_item do 3 | payment nil 4 | buyable nil 5 | price 1500 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/mailers/payment_mailer.rb: -------------------------------------------------------------------------------- 1 | class PaymentMailer < ApplicationMailer 2 | 3 | def notify_success(payment) 4 | end 5 | 6 | def notify_failure(payment) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180207163810_add_name_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNameToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180326183937_add_role_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRoleToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :role, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/day_revenues.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :day_revenue do 3 | day "2018-04-15" 4 | ticket_count 1 5 | price "" 6 | discounts "" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/factories/shopping_carts.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :shopping_cart do 3 | user nil 4 | address nil 5 | shipping_method 1 6 | discount_code nil 7 | end 8 | end -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | 3 | def mailer_options 4 | [["None", :electronic], ["Standard", :standard], ["Overnight", :overnight]] 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/tickets.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :ticket do 3 | user nil 4 | performance nil 5 | status 0 6 | access 0 7 | price 100 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/mailers/previews/payment_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/payment_mailer 2 | class PaymentMailerPreview < ActionMailer::Preview 3 | 4 | end 5 | -------------------------------------------------------------------------------- /spec/support/capybara.rb: -------------------------------------------------------------------------------- 1 | require 'capybara/poltergeist' 2 | require 'capybara-screenshot/rspec' 3 | 4 | Capybara.asset_host = "http://localhost:3000" 5 | Capybara.javascript_driver = :poltergeist -------------------------------------------------------------------------------- /config/initializers/tax_cloud.rb: -------------------------------------------------------------------------------- 1 | TaxCloud.configure do |config| 2 | config.api_login_id = Rails.application.secrets.tax_cloud_login 3 | config.api_key = Rails.application.secrets.tax_cloud_key 4 | end -------------------------------------------------------------------------------- /db/migrate/20180310154635_add_stripe_customer_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddStripeCustomerToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :stripe_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/performances.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :performance do 3 | event nil 4 | start_time "2018-02-08 14:25:18" 5 | end_time "2018-02-08 14:25:18" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /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/20180307143259_add_reference_to_ticket.rb: -------------------------------------------------------------------------------- 1 | class AddReferenceToTicket < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :tickets, :payment_reference, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /db/migrate/20180409123243_add_authy_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddAuthyToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | change_table :users do |t| 4 | t.string :authy_id 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/addresses.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :address do 3 | address_1 "1060 W. Addison" 4 | address_2 "" 5 | city "Chicago" 6 | state "IL" 7 | zip "60613" 8 | end 9 | end -------------------------------------------------------------------------------- /spec/factories/affiliates.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :affiliate do 3 | name "MyString" 4 | user nil 5 | country "MyString" 6 | stripe_id "MyString" 7 | tag "MyString" 8 | end 9 | end -------------------------------------------------------------------------------- /spec/support/vcr.rb: -------------------------------------------------------------------------------- 1 | VCR.configure do |config| 2 | config.cassette_library_dir = "spec/vcr_cassettes" 3 | config.hook_into :webmock 4 | config.configure_rspec_metadata! 5 | config.ignore_localhost = true 6 | end -------------------------------------------------------------------------------- /app/models/event.rb: -------------------------------------------------------------------------------- 1 | class Event < ApplicationRecord 2 | 3 | has_paper_trail 4 | 5 | has_many :performances, dependent: :destroy 6 | accepts_nested_attributes_for :performances, allow_destroy: true 7 | end 8 | -------------------------------------------------------------------------------- /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 | channel_prefix: snow_globe_production 11 | -------------------------------------------------------------------------------- /app/controllers/events_controller.rb: -------------------------------------------------------------------------------- 1 | class EventsController < ApplicationController 2 | 3 | def index 4 | @events = Event.all 5 | end 6 | 7 | def show 8 | @event = Event.find(params[:id]) 9 | end 10 | end -------------------------------------------------------------------------------- /app/views/users/sessions/two_factor.html.slim: -------------------------------------------------------------------------------- 1 | h2 Verify Your Code 2 | 3 | = form_tag(users_sessions_verify_path) do 4 | .form-inputs 5 | = text_field_tag(:token) 6 | 7 | .form-actions 8 | = submit_tag("Enter Token") 9 | -------------------------------------------------------------------------------- /db/migrate/20180416115829_add_partial_prices_to_payment.rb: -------------------------------------------------------------------------------- 1 | class AddPartialPricesToPayment < ActiveRecord::Migration[5.1] 2 | def change 3 | change_table :payments do |t| 4 | t.json :partials 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/features/visitors/home_page_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.feature "Home page" do 4 | scenario "visit the home page" do 5 | visit root_path 6 | 7 | expect(page).to have_content("Welcome") 8 | end 9 | end -------------------------------------------------------------------------------- /app/exceptions/pre_existing_payment_exception.rb: -------------------------------------------------------------------------------- 1 | class PreExistingPaymentException < StandardError 2 | 3 | attr_accessor :payment 4 | 5 | def initialize(payment, message = nil) 6 | super(message) 7 | @purchase = payment 8 | end 9 | end -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/policies/payment_policy.rb: -------------------------------------------------------------------------------- 1 | class PaymentPolicy 2 | 3 | attr_reader :user, :record 4 | 5 | def initialize(user, record) 6 | @user = user 7 | @record = record 8 | end 9 | 10 | def refund? 11 | user.admin? 12 | end 13 | end -------------------------------------------------------------------------------- /db/migrate/20180416140942_add_shipping_method_to_purchase.rb: -------------------------------------------------------------------------------- 1 | class AddShippingMethodToPurchase < ActiveRecord::Migration[5.1] 2 | def change 3 | change_table :payments do |t| 4 | t.integer :shipping_method, default: 0 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require bootstrap-sprockets 3 | //= require rails-ujs 4 | //= require turbolinks 5 | //= require jquery.payment 6 | //= require jquery.cardswipe 7 | //= require edits_affiliate 8 | //= require_tree . 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/factories/discount_codes.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :discount_code do 3 | code "MyString" 4 | percentage 1 5 | description "MyText" 6 | minimum_amount nil 7 | maximum_discount nil 8 | max_uses 1 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/workflows/stripe_handler/null_handler.rb: -------------------------------------------------------------------------------- 1 | module StripeHandler 2 | 3 | class NullHandler 4 | 5 | def initialzie(event) 6 | end 7 | 8 | def run 9 | end 10 | 11 | def success 12 | true 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /app/jobs/notify_tax_cloud_job.rb: -------------------------------------------------------------------------------- 1 | class NotifyTaxCloudJob < ActiveJob::Base 2 | 3 | include Rollbar::ActiveJob 4 | 5 | queue_as :default 6 | 7 | def perform(payment) 8 | workflow = NotifiesTaxCloud.new(payment) 9 | workflow.run 10 | end 11 | end -------------------------------------------------------------------------------- /spec/factories/payments.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :payment do 3 | user nil 4 | price 100 5 | status 1 6 | reference "MyString" 7 | payment_method "MyString" 8 | response_id "MyString" 9 | full_response "" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/policies/payment_line_item_policy.rb: -------------------------------------------------------------------------------- 1 | class PaymentLineItemPolicy 2 | 3 | attr_reader :user, :record 4 | 5 | def initialize(user, record) 6 | @user = user 7 | @record = record 8 | end 9 | 10 | def refund? 11 | user.admin? 12 | end 13 | end -------------------------------------------------------------------------------- /spec/factories/subscriptions.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :subscription do 3 | user nil 4 | plan nil 5 | start_date "2018-03-10" 6 | end_date "2018-03-10" 7 | status 1 8 | payment_method "MyString" 9 | remote_id "MyString" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/stripe.rb: -------------------------------------------------------------------------------- 1 | Stripe.api_key = Rails.application.secrets.stripe_secret_key 2 | raise "Missing Stripe API Key" unless Stripe.api_key 3 | STRIPE_JS_HOST = "https://js.stripe.com".freeze unless defined? STRIPE_JS_HOST 4 | STRIPE_JS_FILE = Rails.env.development? ? "stripe-debug.js" : "" -------------------------------------------------------------------------------- /app/exceptions/unathorized_purchase_exception.rb: -------------------------------------------------------------------------------- 1 | class UnathorizedPurchaseException < StandardError 2 | 3 | attr_accessor :message, :user, :expected_purchase_cents, :expected_ticket_ids 4 | 5 | def initialize(message = nil, user:) 6 | super(message) 7 | @user = user 8 | end 9 | end -------------------------------------------------------------------------------- /app/jobs/notify_tax_cloud_of_refund_job.rb: -------------------------------------------------------------------------------- 1 | class NotifyTaxCloudOfRefundJob < ActiveJob::Base 2 | 3 | include Rollbar::ActiveJob 4 | 5 | queue_as :default 6 | 7 | def perform(payment) 8 | workflow = NotifiesTaxCloudOfRefund.new(payment) 9 | workflow.run 10 | end 11 | end -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /app/controllers/discount_codes_controller.rb: -------------------------------------------------------------------------------- 1 | class DiscountCodesController < ApplicationController 2 | 3 | def create 4 | workflow = AddsDiscountCodeToCart.new( 5 | user: current_user, code: params[:discount_code]) 6 | workflow.run 7 | redirect_to shopping_cart_path 8 | end 9 | end -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20180208122215_create_events.rb: -------------------------------------------------------------------------------- 1 | class CreateEvents < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :events do |t| 4 | t.string :name 5 | t.text :description 6 | t.string :image_url 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/factories/plans.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :plan do 3 | remote_id "MyString" 4 | nickname "MyString" 5 | price_cents 10_000 6 | interval 2 7 | interval_count 1 8 | tickets_allowed 1 9 | ticket_category "MyString" 10 | status 1 11 | description "MyText" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/address.rb: -------------------------------------------------------------------------------- 1 | class Address < ApplicationRecord 2 | 3 | validates :address_1, presence: true 4 | validates :city, presence: true 5 | validates :state, presence: true 6 | validates :zip, presence: true 7 | 8 | def all_fields 9 | [address_1, address_2, city, state, zip].compact.join(", ") 10 | end 11 | end -------------------------------------------------------------------------------- /db/migrate/20180208122518_create_performances.rb: -------------------------------------------------------------------------------- 1 | class CreatePerformances < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :performances do |t| 4 | t.references :event, foreign_key: true 5 | t.datetime :start_time 6 | t.datetime :end_time 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20180415091739_create_day_revenues.rb: -------------------------------------------------------------------------------- 1 | class CreateDayRevenues < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :day_revenues do |t| 4 | t.date :day 5 | t.integer :ticket_count 6 | t.monetize :price 7 | t.monetize :discounts 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/refund_charge_job.rb: -------------------------------------------------------------------------------- 1 | class RefundChargeJob < ActiveJob::Base 2 | 3 | include Rollbar::ActiveJob 4 | 5 | queue_as :default 6 | 7 | def perform(refundable_id:) 8 | refundable = Payment.find(refundable_id) 9 | workflow = CreatesStripeRefund.new(payment_to_refund: refundable) 10 | workflow.run 11 | end 12 | end -------------------------------------------------------------------------------- /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 += 5 | [:password, :password_confirmation, :credit_card_number, 6 | :expiration_month, :expiration_year, :cvc] 7 | -------------------------------------------------------------------------------- /app/models/null_day_revenue.rb: -------------------------------------------------------------------------------- 1 | class NullDayRevenue < DayRevenue 2 | 3 | attr_accessor :date, :ticket_count 4 | 5 | def initialize(date) 6 | @date = date 7 | end 8 | 9 | def day 10 | date 11 | end 12 | 13 | def price 14 | Money.new(0) 15 | end 16 | 17 | def discounts 18 | Money.new(0) 19 | end 20 | end -------------------------------------------------------------------------------- /app/models/performance.rb: -------------------------------------------------------------------------------- 1 | class Performance < ApplicationRecord 2 | 3 | has_paper_trail 4 | 5 | belongs_to :event 6 | has_many :tickets 7 | 8 | def unsold_tickets(count) 9 | tickets.where(status: "unsold").limit(count) 10 | end 11 | 12 | def name 13 | "#{event.name} #{start_time.to_s(:short)}" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/templates/slim/scaffold/_form.html.slim: -------------------------------------------------------------------------------- 1 | = simple_form_for(@<%= singular_table_name %>) do |f| 2 | = f.error_notification 3 | 4 | .form-inputs 5 | <%- attributes.each do |attribute| -%> 6 | = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> 7 | <%- end -%> 8 | 9 | .form-actions 10 | = f.button :submit 11 | -------------------------------------------------------------------------------- /app/admin/discount_codes.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register DiscountCode do 2 | permit_params :code, :percentage 3 | 4 | filter :code 5 | filter :percentage 6 | filter :description 7 | 8 | index do 9 | selectable_column 10 | id_column 11 | column :code 12 | column :percentage 13 | column :description 14 | actions 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/views/subscriptions/edit.html.slim: -------------------------------------------------------------------------------- 1 | h1 Select new subscription plan: 2 | 3 | = form_tag(subscription_path(@subscription), method: :patch) do 4 | - Plan.all.each do |plan| 5 | .form-group 6 | = plan.nickname 7 | = radio_button_tag(:new_plan, plan.id, false) 8 | 9 | .form-group 10 | = submit_tag("Change plan", id: "change_subscription_plan") -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe User do 4 | 5 | before(:each) { @user = User.new(email: "user@example.com") } 6 | 7 | subject { @user } 8 | 9 | it { is_expected.to respond_to(:email) } 10 | 11 | it "#email returns a string" do 12 | expect(@user.email).to match("user@example.com") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20180217152755_create_payment_line_items.rb: -------------------------------------------------------------------------------- 1 | class CreatePaymentLineItems < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :payment_line_items do |t| 4 | t.references :payment, foreign_key: true 5 | t.references :buyable, polymorphic: true 6 | t.monetize :price 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VENDOR_PATH = File.expand_path('..', __dir__) 3 | Dir.chdir(VENDOR_PATH) do 4 | begin 5 | exec "yarnpkg #{ARGV.join(" ")}" 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/models/stripe_token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe StripeToken, :vcr do 4 | it "calls stripe to get a token" do 5 | token = StripeToken.new( 6 | credit_card_number: "4242424242424242", expiration_month: "12", 7 | expiration_year: Time.zone.now.year + 1, cvc: "123") 8 | 9 | expect(token.id).to start_with("tok_") 10 | end 11 | end -------------------------------------------------------------------------------- /app/controllers/pay_pal_payments_controller.rb: -------------------------------------------------------------------------------- 1 | class PayPalPaymentsController < ApplicationController 2 | 3 | def approved 4 | workflow = ExecutesPayPalPayment.new( 5 | payment_id: params[:paymentId], 6 | token: params[:token], 7 | payer_id: params[:PayerID]) 8 | workflow.run 9 | redirect_to payment_path(id: workflow.payment.reference) 10 | end 11 | end -------------------------------------------------------------------------------- /app/models/concerns/has_reference.rb: -------------------------------------------------------------------------------- 1 | module HasReference 2 | 3 | extend ActiveSupport::Concern 4 | 5 | module ClassMethods 6 | 7 | def generate_reference(length: 10, attribute: :reference) 8 | loop do 9 | result = SecureRandom.hex(length) 10 | return result unless exists?(attribute => result) 11 | end 12 | end 13 | 14 | end 15 | end -------------------------------------------------------------------------------- /db/migrate/20180208123252_create_tickets.rb: -------------------------------------------------------------------------------- 1 | class CreateTickets < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :tickets do |t| 4 | t.references :user, foreign_key: true 5 | t.references :performance, foreign_key: true 6 | t.integer :status 7 | t.integer :access 8 | t.monetize :price 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /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/exceptions/tax_validity_exception.rb: -------------------------------------------------------------------------------- 1 | class TaxValidityException < StandardError 2 | 3 | attr_accessor :message, :payment_id, :expected_taxes, :paid_taxes 4 | 5 | def initialize(message = nil, 6 | payment_id:, expected_taxes:, paid_taxes:) 7 | super(message) 8 | @payment_id = payment_id 9 | @expected_taxes = expected_taxes 10 | @paid_taxes = paid_taxes 11 | end 12 | end -------------------------------------------------------------------------------- /db/migrate/20180422155758_more_affiliate_fields.rb: -------------------------------------------------------------------------------- 1 | class MoreAffiliateFields < ActiveRecord::Migration[5.1] 2 | def change 3 | change_table :affiliates do |t| 4 | t.boolean :stripe_charges_enabled, default: false 5 | t.boolean :stripe_transfers_enabled, default: false 6 | t.string :stripe_disabled_reason 7 | t.datetime :stripe_validation_due_by 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/models/plan_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Plan, type: :model do 4 | let(:plan) { build_stubbed(:plan) } 5 | 6 | context "end date" do 7 | it "calculates daily end date" do 8 | plan.interval = "day" 9 | date = Date.parse("Mar 10, 2018") 10 | 11 | expect(plan.end_date_from(date)).to eq(Date.parse("Mar 11, 2018")) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/admin/plans.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register Plan do 2 | filter :nickname 3 | filter :ticket_category 4 | filter :status 5 | 6 | index do 7 | selectable_column 8 | id_column 9 | column :remote_id 10 | column :nickname 11 | column :price 12 | column :interval 13 | column :tickets_allowed 14 | column :ticket_category 15 | column :status 16 | actions 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/jobs/executes_stripe_payment_job.rb: -------------------------------------------------------------------------------- 1 | class ExecutesStripePaymentJob < ActiveJob::Base 2 | 3 | queue_as :default 4 | 5 | rescue_from(PreExistingPaymentException) do |exception| 6 | Rollbar.error(exception) 7 | end 8 | 9 | def perform(payment, stripe_token) 10 | charge_action = ExecutesStripePayment.new(payment: payment, 11 | stripe_token: stripe_token) 12 | charge_action.run 13 | end 14 | end -------------------------------------------------------------------------------- /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/20180416140455_create_shopping_carts.rb: -------------------------------------------------------------------------------- 1 | class CreateShoppingCarts < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :shopping_carts do |t| 4 | t.references :user, foreign_key: true 5 | t.references :address, foreign_key: true 6 | t.integer :shipping_method, default: 0 7 | t.references :discount_code, foreign_key: true 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/features/users/user_delete_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.feature "User delete", :devise do 4 | scenario "user can delete own account" do 5 | user = FactoryBot.create(:user) 6 | sign_in(user.email, user.password) 7 | 8 | visit edit_user_registration_path(user) 9 | click_button("Cancel my account") 10 | 11 | expect(page).to have_content(I18n.t("devise.registrations.destroyed")) 12 | end 13 | end -------------------------------------------------------------------------------- /spec/features/users/sign_out_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.feature "Sign out", :devise do 4 | scenario "user signs out successfully" do 5 | user = FactoryBot.create(:user) 6 | sign_in(user.email, user.password) 7 | expect(page).to have_content(I18n.t("devise.sessions.signed_in")) 8 | 9 | click_link("Sign out") 10 | 11 | expect(page).to have_content(I18n.t("devise.sessions.signed_out")) 12 | end 13 | end -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/mail_interceptor.rb: -------------------------------------------------------------------------------- 1 | unless Rails.env.test? || Rails.env.production? 2 | 3 | options = {forward_emails_to: "alex.khlipun@icloud.com", 4 | deliver_emails_to: ["@snowglobetheater.com"]} 5 | 6 | interceptor = MailInterceptor::Interceptor.new(options) 7 | 8 | ActionMailer::Base.register_interceptor(interceptor) 9 | 10 | EmailPrefixer.configure do |config| 11 | config.application_name = "Snow Globe" 12 | end 13 | end -------------------------------------------------------------------------------- /app/exceptions/charge_setup_validity_exception.rb: -------------------------------------------------------------------------------- 1 | class ChargeSetupValidityException < StandardError 2 | 3 | attr_accessor :message, :user, :expected_purchase_cents, :expected_ticket_ids 4 | 5 | def initialization(message = nil, 6 | user:, expected_purchase_cents:, expected_ticket_ids:) 7 | super(message) 8 | @user = user 9 | @expected_purchase_cents = expected_purchase_cents 10 | @expected_ticket_ids = expected_ticket_ids 11 | end 12 | end -------------------------------------------------------------------------------- /db/migrate/20180310161419_create_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class CreateSubscriptions < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :subscriptions do |t| 4 | t.references :user, foreign_key: true 5 | t.references :plan, foreign_key: true 6 | t.date :start_date 7 | t.date :end_date 8 | t.integer :status 9 | t.string :payment_method 10 | t.string :remote_id 11 | 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/addresses_controller.rb: -------------------------------------------------------------------------------- 1 | class AddressesController < ApplicationController 2 | 3 | def new 4 | @address = Address.new 5 | end 6 | 7 | def create 8 | workflow = AddsShippingToCart.new( 9 | user: current_user, address: params[:address].permit!, 10 | method: params[:shipping_method]) 11 | workflow.run 12 | if workflow.success? 13 | redirect_to shopping_cart_path 14 | else 15 | render :new 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /app/models/subscription_cart.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionCart 2 | 3 | attr_accessor :user 4 | 5 | def initialize(user) 6 | @user = user 7 | end 8 | 9 | def subscriptions 10 | @subscriptions ||= user.subscriptions_in_cart 11 | end 12 | 13 | def total_cost 14 | subscriptions.map(&:plan).map(&:price).sum 15 | end 16 | 17 | def item_ids 18 | subscriptions.map(&:id) 19 | end 20 | 21 | def item_attribute 22 | :subscription_ids 23 | end 24 | end -------------------------------------------------------------------------------- /app/views/plans/index.html.slim: -------------------------------------------------------------------------------- 1 | h2 Subscription Plans 2 | 3 | - @plans.each do |plan| 4 | .plan(id=dom_id(plan)) 5 | 6 | h3.plan-name= plan.nickname 7 | 8 | .plan-description= plan.description 9 | .plan-cost= humanized_money_with_symbol(plan.price) 10 | 11 | = form_tag(subscription_cart_path, method: :patch) do 12 | = hidden_field_tag("plan_id", plan.id) 13 | .form-group 14 | = submit_tag("Add to Cart", class: "btn btn-default", id: "add-to-cart") 15 | -------------------------------------------------------------------------------- /db/migrate/20180310143147_create_plans.rb: -------------------------------------------------------------------------------- 1 | class CreatePlans < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :plans do |t| 4 | t.string :remote_id 5 | t.string :nickname 6 | t.monetize :price 7 | t.integer :interval 8 | t.integer :interval_count 9 | t.integer :tickets_allowed 10 | t.string :ticket_category 11 | t.integer :status 12 | t.text :description 13 | 14 | t.timestamps 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/subscription_carts_controller.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionCartsController < ApplicationController 2 | 3 | def show 4 | @cart = SubscriptionCart.new(current_user) 5 | end 6 | 7 | def update 8 | plan = Plan.find(params[:plan_id]) 9 | workflow = AddsPlanToCart.new(user: current_user, plan: plan) 10 | workflow.run 11 | if workflow.result 12 | redirect_to subscription_cart_path 13 | else 14 | redirect_to plans_path 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /db/migrate/20180217152612_create_payments.rb: -------------------------------------------------------------------------------- 1 | class CreatePayments < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :payments do |t| 4 | t.references :user, foreign_key: true 5 | t.monetize :price 6 | t.integer :status 7 | t.string :reference 8 | t.string :payment_method 9 | t.string :response_id 10 | t.json :full_response 11 | 12 | t.timestamps 13 | end 14 | 15 | add_index :payments, :reference 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20180416135846_create_addresses.rb: -------------------------------------------------------------------------------- 1 | class CreateAddresses < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :addresses do |t| 4 | t.string :address_1 5 | t.string :address_2 6 | t.string :city 7 | t.string :state 8 | t.string :zip 9 | 10 | t.timestamps 11 | end 12 | 13 | change_table :payments do |t| 14 | t.integer :billing_address_id 15 | t.integer :shipping_address_id 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/tasks/migrate_discount_to_partials.rake: -------------------------------------------------------------------------------- 1 | namespace :snow_globe do 2 | task migrate_discounts: :environment do 3 | Payment.transaction do 4 | Payment.all.each do |payment| 5 | partials = {} 6 | if payment.discount_cents.positive? 7 | partials[:discount_cents] = -payment.discount_cents 8 | end 9 | partials[:ticket_cents] = payment.tickets.map(&:price_cents) 10 | payment.update(partials: partials) 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /spec/models/performance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Performance, type: :model do 4 | describe "#unsold_tickets" do 5 | let(:event) { create(:event) } 6 | let(:performance) { create(:performance, event: event) } 7 | let(:unsold_ticket) { create( 8 | :ticket, status: "unsold", performance: performance) } 9 | 10 | it "finds unsold tickets" do 11 | expect(performance.unsold_tickets(1)).to eq([unsold_ticket]) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/workflows/adds_plan_to_cart.rb: -------------------------------------------------------------------------------- 1 | class AddsPlanToCart 2 | 3 | attr_accessor :user, :plan, :result 4 | 5 | def initialize(user:, plan:) 6 | @user = user 7 | @plan = plan 8 | @result = nil 9 | end 10 | 11 | def run 12 | @result = Subscription.create!( 13 | user: user, plan: plan, 14 | start_date: Time.zone.now.to_date, 15 | end_date: plan.end_date_from, 16 | status: :waiting) 17 | end 18 | 19 | def success? 20 | result.valid? 21 | end 22 | end -------------------------------------------------------------------------------- /db/migrate/20180327143530_refund_columns.rb: -------------------------------------------------------------------------------- 1 | class RefundColumns < ActiveRecord::Migration[5.1] 2 | def change 3 | change_table :payments do |t| 4 | t.references :original_payment, index: true 5 | t.references :administrator, index: true 6 | end 7 | 8 | change_table :payment_line_items do |t| 9 | t.references :original_line_item, index: true 10 | t.references :administrator, index: true 11 | t.integer :refund_status, default: 0 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | 3 | before_action :load_user 4 | 5 | def show 6 | end 7 | 8 | def edit 9 | end 10 | 11 | def update 12 | @user.update(user_params) 13 | authorize(@user) 14 | render :show 15 | end 16 | 17 | private 18 | 19 | def load_user 20 | @user = User.find(params[:id]) 21 | authorize(@user) 22 | end 23 | 24 | def user_params 25 | params.require(:user).permit(:name) 26 | end 27 | end -------------------------------------------------------------------------------- /spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:suite) do 3 | DatabaseCleaner.clean_with(:truncation) 4 | end 5 | 6 | config.before(:each) do 7 | DatabaseCleaner.strategy = :transaction 8 | end 9 | 10 | config.before(:each, js: true) do 11 | DatabaseCleaner.strategy = :truncation 12 | end 13 | 14 | config.before(:each) do 15 | DatabaseCleaner.start 16 | end 17 | 18 | config.append_after(:each) do 19 | DatabaseCleaner.clean 20 | end 21 | end -------------------------------------------------------------------------------- /app/models/plan.rb: -------------------------------------------------------------------------------- 1 | class Plan < ApplicationRecord 2 | 3 | has_paper_trail 4 | 5 | enum status: {inactive: 0, active: 1} 6 | enum interval: {day: 0, week: 1, month: 2, year: 3} 7 | 8 | monetize :price_cents 9 | 10 | def remote_plan 11 | @remote_plan ||= Stripe::Plan.retrieve(remote_id) 12 | end 13 | 14 | def end_date_from(date = nil) 15 | date ||= Date.current.to_date 16 | interval_count.send(interval).from_now(date) 17 | end 18 | 19 | def name 20 | nickname 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/views/layouts/_navigation.html.slim: -------------------------------------------------------------------------------- 1 | nav.navbar.navbar-default.navbar-static-top 2 | .container 3 | .navbar-header 4 | button.navbar-toggle( 5 | data-target=".navbar-collapse" data-toggle="collapse") 6 | span.sr-only Toggle navigation 7 | span.icon-bar 8 | span.icon-bar 9 | span.icon-bar 10 | = link_to("Snow Globe Theater", root_path, class: "navbar-brand") 11 | .collapse.navbar-collapse 12 | ul.nav.navbar-nav 13 | = render "layouts/navigation_links" 14 | -------------------------------------------------------------------------------- /app/views/subscription_carts/show.html.slim: -------------------------------------------------------------------------------- 1 | h1 Subscription Cart 2 | 3 | table.table 4 | thead 5 | tr 6 | th Plan 7 | th Total Price 8 | tbody 9 | - @cart.subscriptions.each do |subscription| 10 | tr(id=dom_id(subscription)) 11 | td= subscription.plan.nickname 12 | td.subtotal= humanized_money_with_symbol(subscription.plan.price) 13 | 14 | h3 Total #{humanized_money_with_symbol(@cart.total_cost)} 15 | 16 | h2 Checkout 17 | 18 | = render "shopping_carts/credit_card_info" 19 | -------------------------------------------------------------------------------- /app/policies/user_policy.rb: -------------------------------------------------------------------------------- 1 | class UserPolicy 2 | 3 | attr_reader :user, :record 4 | 5 | def initialize(user, record) 6 | @user = user 7 | @record = record 8 | end 9 | 10 | def same_user? 11 | user == record 12 | end 13 | 14 | def show? 15 | same_user? || user.admin? 16 | end 17 | 18 | def update? 19 | same_user? || user.admin? 20 | end 21 | 22 | def edit? 23 | same_user? || user.admin? 24 | end 25 | 26 | def simulate? 27 | user.admin? && !same_user? 28 | end 29 | end -------------------------------------------------------------------------------- /db/migrate/20180325143544_create_active_admin_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateActiveAdminComments < ActiveRecord::Migration::Current 2 | def self.up 3 | create_table :active_admin_comments do |t| 4 | t.string :namespace 5 | t.text :body 6 | t.references :resource, polymorphic: true 7 | t.references :author, polymorphic: true 8 | t.timestamps 9 | end 10 | add_index :active_admin_comments, [:namespace] 11 | 12 | end 13 | 14 | def self.down 15 | drop_table :active_admin_comments 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/models/ticket_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Ticket, type: :model do 4 | describe "#place_in_cart_for" do 5 | it "changes ticket status and set the user" do 6 | user = create(:user) 7 | ticket = create(:ticket, status: "unsold", 8 | performance: create(:performance, event: create(:event))) 9 | 10 | ticket.place_in_cart_for(user) 11 | 12 | expect(ticket.user).to eq(user) 13 | expect(ticket.status).to eq("waiting") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/workflows/notifies_tax_cloud_of_refund.rb: -------------------------------------------------------------------------------- 1 | class NotifiesTaxCloudOfRefund 2 | 3 | attr_accessor :payment 4 | 5 | def initialize(payment) 6 | @payment = payment 7 | @success = false 8 | end 9 | 10 | def tax_calculator 11 | @tax_calculator ||= payment.price_calculator.tax_calculator 12 | end 13 | 14 | def reference 15 | payment.original_payment&.reference || payment.reference 16 | end 17 | 18 | def run 19 | result = tax_calculator.refund(reference) 20 | @success = (result == "OK") 21 | end 22 | end -------------------------------------------------------------------------------- /db/migrate/20180330084649_create_discount_codes.rb: -------------------------------------------------------------------------------- 1 | class CreateDiscountCodes < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :discount_codes do |t| 4 | t.string :code 5 | t.integer :percentage 6 | t.text :description 7 | t.monetize :minimum_amount 8 | t.monetize :maximum_discount 9 | t.integer :max_uses 10 | 11 | t.timestamps 12 | end 13 | 14 | change_table :payments do |t| 15 | t.references :discount_code 16 | t.monetize :discount 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/admin/performances.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register Performance do 2 | filter :event 3 | filter :start_time 4 | filter :end_time 5 | 6 | index do 7 | selectable_column 8 | id_column 9 | column :event 10 | column :start_time 11 | column :end_time 12 | actions 13 | end 14 | 15 | form do |f| 16 | f.semantic_errors(*f.object.errors.keys) 17 | f.inputs do 18 | f.input :event 19 | f.input :start_time, as: :string 20 | f.input :end_time, as: :string 21 | end 22 | f.actions 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /db/migrate/20180409081144_add_object_changes_to_versions.rb: -------------------------------------------------------------------------------- 1 | # This migration adds the optional `object_changes` column, in which PaperTrail 2 | # will store the `changes` diff for each update event. See the readme for 3 | # details. 4 | class AddObjectChangesToVersions < ActiveRecord::Migration[5.1] 5 | # The largest text column available in all supported RDBMS. 6 | # See `create_versions.rb` for details. 7 | TEXT_BYTES = 1_073_741_823 8 | 9 | def change 10 | add_column :versions, :object_changes, :text, limit: TEXT_BYTES 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | /node_modules 17 | /yarn-error.log 18 | 19 | .byebug_history 20 | 21 | .env 22 | -------------------------------------------------------------------------------- /app/models/affiliate.rb: -------------------------------------------------------------------------------- 1 | class Affiliate < ApplicationRecord 2 | 3 | include HasReference 4 | 5 | belongs_to :user, optional: true 6 | 7 | def self.generate_tag 8 | generate_reference(length: 5, attribute: :tag) 9 | end 10 | 11 | def verification_needed? 12 | verification_needed.size.positive? 13 | end 14 | 15 | def verification_form_names 16 | verification_needed.map { |name| convert_form_name(name) } 17 | end 18 | 19 | def convert_form_name(attribute) 20 | "account[#{attribute.gsub('.', '][')}]" 21 | end 22 | end -------------------------------------------------------------------------------- /app/controllers/shopping_carts_controller.rb: -------------------------------------------------------------------------------- 1 | class ShoppingCartsController < ApplicationController 2 | 3 | def show 4 | @cart = ShoppingCart.for(user: current_user) 5 | end 6 | 7 | def update 8 | performance = Performance.find(params[:performance_id]) 9 | workflow = AddsToCart.new( 10 | user: current_user, performance: performance, 11 | count: params[:ticket_count]) 12 | workflow.run 13 | if workflow.success 14 | redirect_to shopping_cart_path 15 | else 16 | redirect_to performance.event 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/workflows/adds_discount_code_to_cart.rb: -------------------------------------------------------------------------------- 1 | class AddsDiscountCodeToCart 2 | 3 | attr_accessor :user, :code 4 | 5 | def initialize(user:, code:) 6 | @user = user 7 | @code = code 8 | @success = false 9 | end 10 | 11 | def shopping_cart 12 | @shopping_cart ||= ShoppingCart.for(user: user) 13 | end 14 | 15 | def discount_code 16 | @discount_code ||= DiscountCode.find_by(code: code) 17 | end 18 | 19 | def run 20 | @success = shopping_cart.update(discount_code: discount_code) 21 | end 22 | 23 | def success? 24 | @success 25 | end 26 | end -------------------------------------------------------------------------------- /app/workflows/adds_to_cart.rb: -------------------------------------------------------------------------------- 1 | class AddsToCart 2 | 3 | attr_accessor :user, :performance, :count, :success 4 | 5 | def initialize(user:, performance:, count:) 6 | @user = user 7 | @performance = performance 8 | @count = count.to_i 9 | @success = false 10 | end 11 | 12 | def run 13 | Ticket.transaction do 14 | tickets = performance.unsold_tickets(count) 15 | return if tickets.size != count 16 | tickets.each { |ticket| ticket.place_in_cart_for(user) } 17 | self.success = tickets.all?(&:valid?) 18 | success 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /app/workflows/stripe_handler/customer_subscription_deleted.rb: -------------------------------------------------------------------------------- 1 | module StripeHandler 2 | 3 | class CustomerSubscriptionDeleted 4 | 5 | attr_accessor :event, :success, :payment 6 | 7 | def initialize(event) 8 | @event = event 9 | @success = false 10 | end 11 | 12 | def remote_subscription 13 | @event.data.object 14 | end 15 | 16 | def subscription 17 | @subscription ||= Subscription.find_by(remote_id: remote_subscription.id) 18 | end 19 | 20 | def run 21 | subscription&.canceled! 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /spec/models/payment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Payment, type: :model do 4 | describe ".generate_reference" do 5 | 6 | before(:example) do 7 | allow(SecureRandom).to receive(:hex).and_return("first", "second") 8 | end 9 | 10 | it "generates a reference" do 11 | expect(Payment.generate_reference).to eq("first") 12 | end 13 | 14 | it "avoids duplicates" do 15 | create(:payment, reference: "first", user: create(:user)) 16 | 17 | expect(Payment.generate_reference).to eq("second") 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/views/layouts/_messages.html.slim: -------------------------------------------------------------------------------- 1 | - flash.each do |name, msg| 2 | - if msg.is_a?(String) 3 | .alert(class="alert-#{name.to_s == "notice" ? "success" : "danger"}") 4 | button.close( 5 | type="button" data-dismiss="alert" aria-hidden="true") × 6 | div(id="flash_#{name}")= msg 7 | 8 | - if simulating_admin_user 9 | .alert.alert-danger 10 | button.close(aria-hidden="true" 11 | data-dismiss=alert type="button") 12 | | × 13 | div 14 | | You are simulating #{current_user.email} 15 | = link_to("End Simulation", user_simulation_path, method: :delete) -------------------------------------------------------------------------------- /spec/support/helpers/session_helpers.rb: -------------------------------------------------------------------------------- 1 | module Features 2 | 3 | module SessionHelpers 4 | 5 | def sign_up_with(email, password, confirmation) 6 | visit new_user_registration_path 7 | fill_in "Email", with: email 8 | fill_in "Password", with: password 9 | fill_in "Password confirmation", with: confirmation 10 | click_button "Sign up" 11 | end 12 | 13 | def sign_in(email, password) 14 | visit new_user_session_path 15 | fill_in "Email", with: email 16 | fill_in "Password", with: password 17 | click_button "Log in" 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /app/controllers/user_simulations_controller.rb: -------------------------------------------------------------------------------- 1 | class UserSimulationsController < ApplicationController 2 | 3 | def create 4 | @user_to_simulate = User.find(params[:id_to_simulate]) 5 | authorize(@user_to_simulate, :simulate?) 6 | session[:admin_id] = current_user.id 7 | sign_in(:user, @user_to_simulate, bypass: true) 8 | redirect_to root_path 9 | end 10 | 11 | def destroy 12 | redirect_to(admin_users_path) && return if simulating_admin_user.nil? 13 | sign_in(:user, simulating_admin_user, bypass: true) 14 | session[:admin_id] = nil 15 | redirect_to admin_users_path 16 | end 17 | end -------------------------------------------------------------------------------- /app/reports/day_revenue_report.rb: -------------------------------------------------------------------------------- 1 | class DayRevenueReport < SimpleDelegator 2 | 3 | include Reportable 4 | 5 | def self.find_collection 6 | result = DayRevenue.all.map { |dr| DayRevenueReport.new(dr) } 7 | result << DayRevenueReport.new(DayRevenue.build_for(Date.yesterday)) 8 | result << DayRevenueReport.new(DayRevenue.build_for(Date.current)) 9 | result.sort_by(&:day) 10 | end 11 | 12 | def initialize(day_revenue) 13 | super(day_revenue) 14 | end 15 | 16 | columns do 17 | column(:day) 18 | column(:price) 19 | column(:discounts) 20 | column(:ticket_count) 21 | end 22 | end -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | 3 | has_paper_trail ignore: %i(sign_in_count current_sign_in_at last_sign_in_at) 4 | 5 | devise :database_authenticatable, :registerable, 6 | :recoverable, :rememberable, :trackable, :validatable 7 | 8 | enum role: {user: 0, vip: 1, admin: 2} 9 | 10 | has_many :tickets 11 | has_many :subscriptions 12 | has_many :affiliates 13 | 14 | attr_accessor :cellphone_number 15 | 16 | def tickets_in_cart 17 | tickets.waiting.all.to_a 18 | end 19 | 20 | def subscriptions_in_cart 21 | subscriptions.waiting.all.to_a 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/workflows/stripe_handler/account_updated.rb: -------------------------------------------------------------------------------- 1 | module StripeHandler 2 | 3 | class AccountUpdated 4 | 5 | attr_accessor :event, :success 6 | 7 | def initialize(event) 8 | @event = event 9 | @success = false 10 | end 11 | 12 | def account 13 | event.data.object 14 | end 15 | 16 | def affiliate 17 | Affiliate.find_by(stripe_id: account.id) 18 | end 19 | 20 | def run 21 | stripe_account = StripeAccount.new(affiliate, account: account) 22 | result = stripe_account.update_affiliate_verification 23 | @success = result 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /spec/workflows/adds_affiliate_account_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe AddsAffiliateAccount, :vcr do 4 | let(:user) { create(:user) } 5 | 6 | describe "creates an affiliate from a user" do 7 | let(:workflow) { AddsAffiliateAccount.new(user: user) } 8 | 9 | it "creates an affiliate account with the required information" do 10 | workflow.run 11 | 12 | expect(workflow.affiliate).to have_attributes( 13 | name: user.name, country: "US", 14 | stripe_id: a_string_starting_with("acct_"), tag: a_truthy_value) 15 | expect(workflow).to be_a_success 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /spec/workflows/adds_plan_to_cart_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe AddsPlanToCart do 4 | let(:user) { create(:user) } 5 | let(:plan) { create(:plan) } 6 | let(:action) { AddsPlanToCart.new(user: user, plan: plan) } 7 | 8 | describe "happy path adding plans" do 9 | it "adds a ticket to a cart" do 10 | action.run 11 | 12 | expect(action).to be_a_success 13 | expect(action.result).to have_attributes( 14 | user: user, plan: plan, start_date: Date.current.to_date, 15 | end_date: Date.current.to_date + 1.month) 16 | expect(action.result).to be_waiting 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /app/models/ticket.rb: -------------------------------------------------------------------------------- 1 | class Ticket < ApplicationRecord 2 | 3 | has_paper_trail 4 | 5 | belongs_to :user, optional: true 6 | belongs_to :performance 7 | has_one :event, through: :performance 8 | 9 | monetize :price_cents 10 | 11 | enum status: {unsold: 0, waiting: 1, purchased: 2, pending: 3, 12 | refund_pending: 4, refunded: 5} 13 | enum access: {general: 0} 14 | 15 | def place_in_cart_for(user) 16 | update(status: :waiting, user: user) 17 | end 18 | 19 | def refund_successful 20 | refunded! 21 | new_ticket = dup 22 | new_ticket.unsold! 23 | new_ticket.payment_reference = nil 24 | new_ticket.save 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /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 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /app/views/events/show.html.slim: -------------------------------------------------------------------------------- 1 | h1= @event.name 2 | 3 | h2 Available Performances 4 | - @event.performances.each do |performance| 5 | div(id=dom_id(performance)) 6 | .row 7 | = form_tag(shopping_cart_path, method: :patch) do 8 | = hidden_field_tag("performance_id", performance.id) 9 | .col-md-2 10 | h4= performance.start_time.to_date.to_s(:long) 11 | .col-md-1 12 | .form-group 13 | = select_tag("ticket_count", 14 | options_for_select((1..10).to_a), class: "form-control") 15 | .col-md-3 16 | .form-group 17 | = submit_tag("Add to Cart", 18 | class: "btn btn-primary", id: "add-to-cart") 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/active_admin.scss: -------------------------------------------------------------------------------- 1 | // SASS variable overrides must be declared before loading up Active Admin's styles. 2 | // 3 | // To view the variables that Active Admin provides, take a look at 4 | // `app/assets/stylesheets/active_admin/mixins/_variables.scss` in the 5 | // Active Admin source. 6 | // 7 | // For example, to change the sidebar width: 8 | // $sidebar-width: 242px; 9 | 10 | // Active Admin's got SASS! 11 | @import "active_admin/mixins"; 12 | @import "active_admin/base"; 13 | @import "wigu/active_admin_theme"; 14 | 15 | // Overriding any non-variable SASS must be done after the fact. 16 | // For example, to change the default status-tag color: 17 | // 18 | // .status_tag { background: #6090DB; } 19 | -------------------------------------------------------------------------------- /app/views/affiliates/edit.html.slim: -------------------------------------------------------------------------------- 1 | h1 Affiliate Validation 2 | 3 | h2 Due by: #{@affiliate.stripe_validation_due_by} 4 | 5 | = form_for(@affiliate, html: {id: "affiliate-form"}) do |f| 6 | 7 | - @affiliate.verification_form_names.each do |field_name| 8 | - if field_name == "account[external_account]" 9 | = render "affiliates/edit/bank_account_form" 10 | - else 11 | .row 12 | .form-group 13 | .col-sm-3 14 | = label_tag(field_name.humanize, field_name, class: "control-label") 15 | .col-sm-5 16 | = text_field_tag(field_name, "", class: "form-control") 17 | .row 18 | .form-group 19 | = f.submit("Make Me an Affiliate", class: "btn btn-default") 20 | -------------------------------------------------------------------------------- /app/views/payments/show.html.slim: -------------------------------------------------------------------------------- 1 | h1 Payment Number: #{@reference} 2 | 3 | - if @payment 4 | table.table 5 | thead 6 | tr 7 | th Event 8 | th Date 9 | th Price 10 | tbody 11 | - @payment.payment_line_items.each do |payment_line_item| 12 | - ticket = payment_line_item.buyable 13 | tr.purchased_ticket(id=dom_id(ticket, :purchased)) 14 | td= ticket.event.name 15 | td= ticket.performance.start_time.to_date.to_s(:long) 16 | td.subtotal= humanized_money_with_symbol(payment_line_item.price) 17 | 18 | h3 Total #{humanized_money_with_symbol(@payment.price)} 19 | 20 | - else 21 | 22 | h2 Payment is pending, check back for updates -------------------------------------------------------------------------------- /app/admin/payment_line_items.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register PaymentLineItem do 2 | actions :all, except: [:new, :edit] 3 | 4 | filter :buyable_type 5 | filter :price_cents 6 | filter :refund_status 7 | filter :administrator_id 8 | filter :created_at 9 | 10 | index do 11 | selectable_column 12 | id_column 13 | column :buyable_type 14 | column :price 15 | column :created_at 16 | actions 17 | end 18 | 19 | action_item :refund, only: :show do 20 | link_to("Refund Payment", 21 | refunds_path(id: payment_line_item.id, type: PaymentLineItem), 22 | method: "POST", 23 | class: "button", 24 | data: {confirm: "Are you sure you want to refund this payment?"}) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /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/workflows/cash_purchases_cart.rb: -------------------------------------------------------------------------------- 1 | class CashPurchasesCart < PreparesCart 2 | 3 | def update_tickets 4 | tickets.each(&:purchased!) 5 | end 6 | 7 | def on_success 8 | @success = true 9 | tickets.each do |ticket| 10 | ticket.update(payment_reference: payment.reference) 11 | end 12 | NotifyTaxCloudJob.perform_later(payment) 13 | end 14 | 15 | def payment_attributes 16 | super.merge( 17 | payment_method: "cash", status: "succeeded", 18 | administrator_id: user.id) 19 | end 20 | 21 | def pre_purchase_valid? 22 | raise UnathorizedPurchaseException.new(user: user) unless user.admin? 23 | true 24 | end 25 | 26 | def on_failure 27 | unpurchase_tickets 28 | end 29 | end -------------------------------------------------------------------------------- /app/views/layouts/_navigation_links.html.slim: -------------------------------------------------------------------------------- 1 | li= link_to("Events", events_path) 2 | li= link_to("Subscription plans", plans_path) 3 | 4 | - if user_signed_in? 5 | 6 | li= link_to("Dashboard", user_path(current_user)) 7 | li= link_to("Edit account", edit_user_registration_path) 8 | li= link_to("Sign out", destroy_user_session_path, method: "delete") 9 | 10 | - else 11 | 12 | li= link_to("Sign in", new_user_session_path) 13 | li= link_to("Sign up", new_user_registration_path) 14 | 15 | - if current_user&.admin? 16 | li.dropdown 17 | a.dropdown-toggle(data-toggle="dropdown" href="#" role="button") 18 | | Reports 19 | span.caret 20 | ul.dropdown-menu 21 | li= link_to("Daily Revenue", daily_revenue_report_path) 22 | -------------------------------------------------------------------------------- /app/workflows/adds_affiliate_to_cart.rb: -------------------------------------------------------------------------------- 1 | class AddsAffiliateToCart 2 | 3 | attr_accessor :user, :tag 4 | 5 | def initialize(user:, tag:) 6 | @user = user 7 | @tag = tag&.downcase 8 | end 9 | 10 | def affiliate 11 | return nil if tag.blank? 12 | @affiliate ||= Affiliate.find_by(tag: tag) 13 | end 14 | 15 | def shopping_cart 16 | @shopping_cart ||= ShoppingCart.for(user: user) 17 | end 18 | 19 | def affiliate_belongs_to_user? 20 | return true unless affiliate 21 | return true unless user 22 | affiliate&.user == user 23 | end 24 | 25 | def run 26 | return unless user 27 | return if affiliate_belongs_to_user? 28 | shopping_cart.update(affiliate: affiliate) 29 | end 30 | end -------------------------------------------------------------------------------- /app/workflows/adds_shipping_to_cart.rb: -------------------------------------------------------------------------------- 1 | class AddsShippingToCart 2 | 3 | attr_accessor :user, :address_fields, :method 4 | 5 | def initialize(user:, address:, method:) 6 | @user = user 7 | @address_fields = address 8 | @method = method 9 | @success = false 10 | end 11 | 12 | def shopping_cart 13 | @shopping_cart ||= ShoppingCart.for(user: user) 14 | end 15 | 16 | def run 17 | ShoppingCart.transaction do 18 | shopping_cart.create_address!(address_fields) 19 | shopping_cart.update!(shipping_method: method) 20 | @success = shopping_cart.valid? 21 | end 22 | rescue ActiveRecord::RecordInvalid 23 | @success = false 24 | end 25 | 26 | def success? 27 | @success 28 | end 29 | end -------------------------------------------------------------------------------- /db/migrate/20180420165023_create_affiliates.rb: -------------------------------------------------------------------------------- 1 | class CreateAffiliates < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :affiliates do |t| 4 | t.string :name 5 | t.references :user, foreign_key: true 6 | t.string :country 7 | t.string :stripe_id 8 | t.string :tag 9 | t.json :verification_needed 10 | t.timestamps 11 | end 12 | 13 | change_table :payments do |t| 14 | t.references :affiliate, foreign_key: true 15 | t.integer :affiliate_payment_cents, default: 0, null: false 16 | t.string :affiliate_payment_currency, default: "USD", null: false 17 | end 18 | 19 | change_table :shopping_carts do |t| 20 | t.references :affiliate 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/reports/purchase_audit.rb: -------------------------------------------------------------------------------- 1 | class PurchaseAudit 2 | 3 | include Reportable 4 | 5 | attr_accessor :payment, :user 6 | 7 | def self.find_collection 8 | Payment.includes(:user).succeeded.find_each.lazy.map do |payment| 9 | PurchaseAudit.new(payment) 10 | end 11 | end 12 | 13 | def initialize(payment) 14 | @payment = payment 15 | @user = payment.user 16 | end 17 | 18 | columns do 19 | column(:reference) { |report| report.payment.reference } 20 | column(:user_email) { |report| report.user.email } 21 | column(:user_stripe_id) { |report| report.user.stripe_id } 22 | column(:price) { |report| report.payment.price.to_f } 23 | column(:total_value) { |report| report.payment.full_value.to_f } 24 | end 25 | end -------------------------------------------------------------------------------- /app/controllers/daily_revenue_reports_controller.rb: -------------------------------------------------------------------------------- 1 | class DailyRevenueReportsController < ApplicationController 2 | 3 | def show 4 | respond_to do |format| 5 | format.html {} 6 | format.csv do 7 | headers["X-Accel-Buffering"] = "no" 8 | headers["Cache-Control"] = "no-cache" 9 | headers["Content-Type"] = "text/csv; charset=utf-8" 10 | headers["Content-Disposition"] = 11 | %(attachment; filename="#{csv_filename}") 12 | headers["Last-Modified"] = Time.zone.now.ctime.to_s 13 | self.response_body = DayRevenueReport.to_csv_enumerator 14 | end 15 | end 16 | end 17 | 18 | private def csv_filename 19 | "daily_revenue_report-#{Time.zone.now.to_date.to_s(:default)}.csv" 20 | end 21 | end -------------------------------------------------------------------------------- /app/workflows/notifies_tax_cloud.rb: -------------------------------------------------------------------------------- 1 | class NotifiesTaxCloud 2 | 3 | attr_accessor :payment 4 | 5 | def initialize(payment) 6 | @payment = payment 7 | @success = false 8 | end 9 | 10 | def tax_calculator 11 | @tax_calculator ||= payment.price_calculator.tax_calculator 12 | end 13 | 14 | def valid_amount? 15 | tax_calculator.tax_amount * 100 == payment.paid_taxes.to_money 16 | end 17 | 18 | def run 19 | if valid_amount? 20 | result = tax_calculator.authorized_with_capture(payment.reference) 21 | @success = (result == "OK") 22 | else 23 | raise TaxValidityException.new( 24 | payment_id: payment.id, expected_taxes: tax_calculator.tax_amount, 25 | paid_taxes: payment.paid_taxes) 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /spec/features/subscriptions/user_adds_plan_to_cart_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "User adds a subscription plan to the cart" do 4 | let(:user) { create(:user) } 5 | let!(:plan) { create(:plan, remote_id: "orchestra_monthly", 6 | nickname: "Orchestra Monthly", price_cents: 30_000, status: :active) } 7 | 8 | before(:example) do 9 | sign_in(user.email, user.password) 10 | end 11 | 12 | scenario "happy path" do 13 | visit plans_path 14 | 15 | within("#plan_#{plan.id}") do 16 | click_on("add-to-cart") 17 | end 18 | 19 | expect(current_url).to match("cart") 20 | within("#subscription_#{user.subscriptions.last.id}") do 21 | expect(page).to have_selector(".subtotal", text: "$300") 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /app/workflows/prepares_cart_for_pay_pal.rb: -------------------------------------------------------------------------------- 1 | class PreparesCartForPayPal < PreparesCart 2 | 3 | attr_accessor :pay_pal_payment 4 | 5 | def update_tickets 6 | tickets.each(&:pending!) 7 | end 8 | 9 | def redirect_on_success_url 10 | pay_pal_payment.redirect_url 11 | end 12 | 13 | def payment_attributes 14 | super.merge(payment_method: "paypal") 15 | end 16 | 17 | def calculate_success 18 | @success = payment.pending? 19 | end 20 | 21 | def on_success 22 | @pay_pal_payment = PayPalPayment.new(payment: payment) 23 | payment.update!(response_id: pay_pal_payment.response_id) 24 | tickets.each do |ticket| 25 | ticket.update(payment_reference: payment.reference) 26 | end 27 | payment.pending! 28 | unpurchase_tickets if payment.failed? 29 | end 30 | end -------------------------------------------------------------------------------- /app/controllers/stripe_webhook_controller.rb: -------------------------------------------------------------------------------- 1 | class StripeWebhookController < ApplicationController 2 | 3 | protect_from_forgery except: :action 4 | 5 | def action 6 | @event_data = JSON.parse(request.body.read) 7 | workflow = workflow_class.new(verify_event) 8 | workflow.run 9 | if workflow.success 10 | render nothing: true 11 | else 12 | render nothing: true, status: 500 13 | end 14 | end 15 | 16 | private 17 | 18 | def verify_event 19 | Stripe::Event.retrieve(@event_data["id"]) 20 | rescue Stripe::InvalidRequestError 21 | nil 22 | end 23 | 24 | def workflow_class 25 | event_type = @event_data["type"] 26 | "StripeHandler::#{event_type.tr('.', '_').camelize}".constantize 27 | rescue NameError 28 | StripeHandler::NullHandler 29 | end 30 | end -------------------------------------------------------------------------------- /app/models/discount_code.rb: -------------------------------------------------------------------------------- 1 | class DiscountCode < ApplicationRecord 2 | 3 | has_paper_trail 4 | 5 | monetize :minimum_amount_cents 6 | monetize :maximum_discount_cents 7 | 8 | def percentage_float 9 | percentage * 1.0 / 100 10 | end 11 | 12 | def multiplier 13 | 1 - percentage_float 14 | end 15 | 16 | def discount_for(subtotal) 17 | return Money.zero unless applies_to_total?(subtotal) 18 | result = subtotal * percentage_float 19 | result = [result, maximum_discount].min if maximum_discount? 20 | result 21 | end 22 | 23 | def maximum_discount? 24 | maximum_discount_cents.present? && maximum_discount > Money.zero 25 | end 26 | 27 | def applies_to_total?(subtotal) 28 | return true if minimum_amount_cents.nil? 29 | subtotal >= minimum_amount 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/models/subscription.rb: -------------------------------------------------------------------------------- 1 | class Subscription < ApplicationRecord 2 | 3 | has_paper_trail 4 | 5 | belongs_to :user 6 | belongs_to :plan 7 | 8 | enum status: {active: 0, inactive: 1, 9 | waiting: 2, pending_initial_payment: 3, 10 | canceled: 4} 11 | 12 | delegate :name, to: :plan 13 | 14 | def make_stripe_payment(stripe_customer) 15 | update!( 16 | payment_method: :stripe, status: :pending_initial_payment, 17 | remote_id: stripe_customer.find_subscription_for(plan)) 18 | end 19 | 20 | def remote_plan_id 21 | plan.remote_id 22 | end 23 | 24 | def update_end_date 25 | update!(end_date: plan.end_date_from) 26 | end 27 | 28 | def currently_active? 29 | active? && (end_date > Date.current) 30 | end 31 | 32 | def performance 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/models/affiliate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Affiliate, type: :model do 4 | let(:affiliate) { Affiliate.new } 5 | 6 | it "converts field names to form attributes" do 7 | expect(affiliate.convert_form_name("attribute")).to eq( 8 | "account[attribute]") 9 | expect(affiliate.convert_form_name("legal_entity.type")).to eq( 10 | "account[legal_entity][type]") 11 | expect(affiliate.convert_form_name("legal_entity.dob.year")).to eq( 12 | "account[legal_entity][dob][year]") 13 | end 14 | 15 | it "gives a list of field names" do 16 | affiliate.verification_needed = [ 17 | "legal_entity.type", "legal_entity.dob.year" 18 | ] 19 | 20 | expect(affiliate.verification_form_names).to eq( 21 | ["account[legal_entity][type]", "account[legal_entity][dob][year]"]) 22 | end 23 | end -------------------------------------------------------------------------------- /app/workflows/updates_affiliate_account.rb: -------------------------------------------------------------------------------- 1 | class UpdatesAffiliateAccount 2 | 3 | attr_accessor :affiliate, :user, :params, :success 4 | 5 | def initialize(affiliate:, user:, params:) 6 | @affiliate = affiliate 7 | @user = user 8 | @params = params 9 | @success = false 10 | end 11 | 12 | def affiliate_belongs_to_user? 13 | return true unless affiliate 14 | return true unless user 15 | affiliate&.user == user 16 | end 17 | 18 | def stripe_account 19 | @stripe_account ||= StripeAccount.new(affiliate) 20 | end 21 | 22 | def run 23 | Affiliate.transaction do 24 | return if user.nil? || affiliate.nil? 25 | return unless affiliate_belongs_to_user? 26 | stripe_account.update(params) 27 | @success = true 28 | end 29 | end 30 | 31 | def success? 32 | @success 33 | end 34 | end -------------------------------------------------------------------------------- /app/policies/application_policy.rb: -------------------------------------------------------------------------------- 1 | class ApplicationPolicy 2 | attr_reader :user, :record 3 | 4 | def initialize(user, record) 5 | @user = user 6 | @record = record 7 | end 8 | 9 | def index? 10 | false 11 | end 12 | 13 | def show? 14 | scope.where(:id => record.id).exists? 15 | end 16 | 17 | def create? 18 | false 19 | end 20 | 21 | def new? 22 | create? 23 | end 24 | 25 | def update? 26 | false 27 | end 28 | 29 | def edit? 30 | update? 31 | end 32 | 33 | def destroy? 34 | false 35 | end 36 | 37 | def scope 38 | Pundit.policy_scope!(user, record.class) 39 | end 40 | 41 | class Scope 42 | attr_reader :user, :scope 43 | 44 | def initialize(user, scope) 45 | @user = user 46 | @scope = scope 47 | end 48 | 49 | def resolve 50 | scope 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/reports/reportable.rb: -------------------------------------------------------------------------------- 1 | module Reportable 2 | 3 | def self.included(base) 4 | base.extend(ClassMethods) 5 | end 6 | 7 | module ClassMethods 8 | 9 | attr_accessor :report_builder 10 | 11 | def columns(options = {}, &block) 12 | self.report_builder = ReportBuilder.new(options, &block) 13 | end 14 | 15 | def to_csv(collection: nil) 16 | report_builder.build(collection || find_collection, 17 | output: "", format: :csv) 18 | end 19 | 20 | def to_json(collection: nil) 21 | result = report_builder.build(collection || find_collection) 22 | result.to_json 23 | end 24 | 25 | def to_csv_enumerator(collection: nil) 26 | Enumerator.new do |y| 27 | report_builder.build(collection || find_collection, 28 | output: y, format: :csv) 29 | end 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /spec/workflows/adds_shipping_to_cart_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe AddsShippingToCart do 4 | let(:user) { create(:user) } 5 | let(:address) { attributes_for(:address) } 6 | let(:workflow) { AddsShippingToCart.new( 7 | user: user, address: address, method: :standard) } 8 | 9 | it "adds shipping to cart" do 10 | workflow.run 11 | cart = ShoppingCart.for(user: user) 12 | 13 | expect(cart.address).to have_attributes(address) 14 | expect(cart).to be_standard 15 | expect(workflow).to be_a_success 16 | end 17 | 18 | it "fails gracefully if a field is missing" do 19 | address.delete(:zip) 20 | workflow.run 21 | cart = ShoppingCart.for(user: user) 22 | 23 | expect(cart.address).to be_nil 24 | expect(cart.shipping_method).to eq("electronic") 25 | expect(workflow).not_to be_a_success 26 | end 27 | end -------------------------------------------------------------------------------- /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/jobs/build_day_revenue_job.rb: -------------------------------------------------------------------------------- 1 | class BuildDayRevenueJob < ApplicationJob 2 | 3 | queue_as :default 4 | 5 | def perform(*_args) 6 | DayRevenue.transaction do 7 | DayRevenue.destroy_all 8 | ActiveRecord::Base.connection.select_all( 9 | %{SELECT date(created_at) as day, 10 | sum(price_cents) as price_cents, 11 | sum(discount_cents) as discounts_cents 12 | FROM "payments" 13 | WHERE "payments"."status" = 1 14 | GROUP BY date(created_at) 15 | HAVING date(created_at) < '#{1.day.ago.to_date}'}).map do |data| 16 | DayRevenue.create(data) 17 | end 18 | DayRevenue.all.each do |day_revenue| 19 | tickets = PaymentLineItem.tickets.no_refund. 20 | where("date(created_at) = ?", day_revenue.day).count 21 | day_revenue.update(ticket_count: tickets) 22 | end 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /spec/models/stripe_account_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe StripeAccount, :vcr do 4 | 5 | describe "updating" do 6 | let(:affiliate_user) { create(:user) } 7 | let(:affiliate_workflow) { 8 | AddsAffiliateAccount.new(user: affiliate_user) } 9 | let(:account) { affiliate_workflow.account } 10 | let(:values) { { 11 | legal_entity: {first_name: "Alexander", 12 | dob: {day: 27, month: 10, year: 1985}}} } 13 | 14 | it "updates the account from a hash" do 15 | affiliate_workflow.run 16 | account.update(values) 17 | 18 | expect(account.account.legal_entity.first_name).to eq("Alexander") 19 | expect(account.account.legal_entity.dob.day).to eq(27) 20 | expect(account.account.legal_entity.dob.month).to eq(10) 21 | expect(account.account.legal_entity.dob.year).to eq(1985) 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /app/models/day_revenue.rb: -------------------------------------------------------------------------------- 1 | class DayRevenue < ApplicationRecord 2 | 3 | monetize :price_cents 4 | monetize :discounts_cents 5 | 6 | def self.for_date(date) 7 | find_by(day: date) || build_for(date) 8 | end 9 | 10 | def self.build_for(date) 11 | revenue = ActiveRecord::Base.connection.select_all( 12 | %{SELECT date(created_at) as day, 13 | sum(price_cents) as price_cents, 14 | sum(discount_cents) as discounts_cents 15 | FROM "payments" 16 | WHERE "payments"."status" = 1 17 | GROUP BY date(created_at) 18 | HAVING date(created_at) = '#{date}'}).map do |data| 19 | DayRevenue.new(data) 20 | end 21 | revenue = revenue.first || NullDayRevenue.new(date) 22 | tickets = PaymentLineItem.tickets.no_refund. 23 | where("date(created_at) = ?", date).count 24 | revenue.ticket_count = tickets 25 | revenue 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/models/day_revenue_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe DayRevenue, type: :model do 4 | let!(:really_old_payment) { create( 5 | :payment, created_at: 1.month.ago, price_cents: 4500) } 6 | let!(:really_old_payment_2) { create( 7 | :payment, created_at: 1.month.ago, price_cents: 1500) } 8 | let!(:old_payment) { create( 9 | :payment, created_at: 2.days.ago, price_cents: 3500) } 10 | let(:performance) { create(:performance, event: create(:event)) } 11 | let!(:ticket) { create(:ticket, performance: performance) } 12 | let!(:payment_line_item) { create( 13 | :payment_line_item, payment: really_old_payment, buyable: ticket, 14 | created_at: 1.month.ago) } 15 | 16 | it "builds data" do 17 | revenue = DayRevenue.build_for(1.month.ago) 18 | 19 | expect(revenue.price_cents).to eq(6000) 20 | expect(revenue.ticket_count).to eq(1) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/workflows/adds_affiliate_account.rb: -------------------------------------------------------------------------------- 1 | class AddsAffiliateAccount 2 | 3 | attr_accessor :user, :affiliate, :success, :tos_checked, :request_ip, :account 4 | 5 | def initialize(user:, tos_checked: nil, request_ip: nil) 6 | @user = user 7 | @tos_checked = tos_checked 8 | @request_ip = request_ip 9 | @success = false 10 | end 11 | 12 | def run 13 | Affiliate.transaction do 14 | @affiliate = Affiliate.create( 15 | user: user, country: "US", 16 | name: user.name, tag: Affiliate.generate_tag) 17 | @affiliate.update(stripe_id: acquire_stripe_id) 18 | account.update_affiliate_verification 19 | end 20 | @success = true 21 | end 22 | 23 | def acquire_stripe_id 24 | @account = StripeAccount.new( 25 | @affiliate, tos_checked: tos_checked, request_ip: request_ip) 26 | account.account.id 27 | end 28 | 29 | def success? 30 | @success 31 | end 32 | end -------------------------------------------------------------------------------- /lib/tasks/plan_creation.rake: -------------------------------------------------------------------------------- 1 | namespace :theater do 2 | 3 | task create_plans: :environment do 4 | plans = [ 5 | {remote_id: "orchestra_monthly", product: {name: "Orchestra Monthly"}, 6 | nickname: "Orchestra Monthly", price_cents: 10_000, interval: "month", 7 | interval_count: 1, tickets_allowed: 1, ticket_category: "Orchestra"}, 8 | {remote_id: "balcony_monthly", product: {name: "Balcony Monthly"}, 9 | nickname: "Balcony Monthly", price_cents: 60_000, interval: "month", 10 | interval_count: 1, tickets_allowed: 1, ticket_category: "Balcony"}, 11 | {remote_id: "vip_monthly", product: {name: "VIP Monthly"}, 12 | nickname: "VIP Monthly", price_cents: 30_000, interval: "month", 13 | interval_count: 1, tickets_allowed: 1, ticket_category: "VIP"} 14 | ] 15 | Plan.transaction do 16 | plans.each { |plan_data| CreatesPlan.new(**plan_data).run } 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /spec/workflows/creates_plan_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe CreatesPlan, :vcr, :aggregate_failures do 4 | 5 | it "creates a plan" do 6 | workflow = CreatesPlan.new( 7 | remote_id: "basic_monthly_#{Time.now.to_i}", 8 | product: {name: "Basic Monthly"}, nickname: "Basic Monthly", 9 | price_cents: 2000, interval: "month", interval_count: 1, 10 | tickets_allowed: 1, ticket_category: "orchestra") 11 | 12 | workflow.run 13 | 14 | expect(workflow.plan).to have_attributes( 15 | remote_id: a_string_starting_with("basic_monthly"), 16 | nickname: "Basic Monthly", price_cents: 2000, interval: "month", 17 | interval_count: 1, tickets_allowed: 1, ticket_category: "orchestra") 18 | expect(workflow.plan.remote_plan).to have_attributes( 19 | id: a_string_starting_with("basic_monthly"), amount: 2000, 20 | interval_count: 1, trial_period_days: nil) 21 | end 22 | end -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | ActiveAdmin.routes(self) 3 | root to: "visitors#index" 4 | 5 | resource :user_simulation, only: %i(create destroy) 6 | 7 | devise_for :users, controllers: { 8 | sessions: "users/sessions" 9 | } 10 | 11 | devise_scope :user do 12 | post "users/sessions/verify" => "Users::SessionsController" 13 | get "users/sessions/two_factor" => "Users::SessionsController" 14 | end 15 | 16 | resources :events 17 | resource :shopping_cart 18 | resource :subscription_cart 19 | resources :payments 20 | resources :users 21 | resources :plans 22 | resources :subscriptions 23 | resources :refunds 24 | resources :discount_codes 25 | resources :addresses 26 | resources :affiliates 27 | 28 | resource :daily_revenue_report 29 | 30 | get "paypal/approved", to: "pay_pal_payments#approved" 31 | 32 | post "stripe/webhook", to: "stripe_webhook#action" 33 | end 34 | -------------------------------------------------------------------------------- /config/paypal.yml: -------------------------------------------------------------------------------- 1 | test: &default 2 | 3 | # Credentials for REST APIs 4 | client_id: EBWKjlELKMYqRNQ6sYvFo64FtaRLRR5BdHEESmha49TM 5 | client_secret: EO422dn3gQLgDbuwqTjzrFgFtaRLRR5BdHEESmha49TM 6 | 7 | # Mode can be 'live' or 'sandbox' 8 | mode: sandbox 9 | 10 | # Credentials for Classic APIs 11 | app_id: APP-80W284485P519543T 12 | username: jb-us-seller_api1.paypal.com 13 | password: WX4WTU3S8MY44S7F 14 | signature: AFcWxV21C7fd0v3bYYYRCpSSRl31A7yDhhsPUU2XhtMoZXsWHFxu-RWy 15 | # # With Certificate 16 | # cert_path: "config/cert_key.pem" 17 | sandbox_email_address: Platform.sdk.seller@gmail.com 18 | 19 | # # IP Address 20 | # ip_address: 127.0.0.1 21 | # # HTTP Proxy 22 | # http_proxy: http://proxy-ipaddress:3129/ 23 | 24 | development: 25 | mode: sandbox 26 | client_id: <%= ENV["PAYPAL_CLIENT_ID"] %> 27 | client_secret: <%= ENV["PAYPAL_CLIENT_SECRET"] %> 28 | 29 | 30 | production: 31 | <<: *default 32 | mode: live 33 | -------------------------------------------------------------------------------- /app/admin/events.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register Event do 2 | permit_params :name, :description, :image_url, 3 | performances_attributes: [:id, :start_time, :end_time, :_destroy] 4 | 5 | config.sort_order = "name_asc" 6 | 7 | filter :name 8 | filter :description 9 | 10 | index do 11 | selectable_column 12 | id_column 13 | column :name 14 | column :description 15 | actions 16 | end 17 | 18 | form do |f| 19 | f.semantic_errors(*f.object.errors.keys) 20 | f.inputs do 21 | f.input :name 22 | f.input :description 23 | f.input :image_url 24 | end 25 | f.inputs do 26 | f.has_many :performances, heading: "Performances", 27 | allow_destroy: true do |fp| 28 | fp.input :start_time, as: :string, placeholder: "YYYY-MM-DD HH:MM" 29 | fp.input :end_time, as: :string, placeholder: "YYYY-MM-DD HH:MM" 30 | end 31 | end 32 | f.actions 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset="utf-8") 5 | meta(name="viewport" content="width=device-width, initial-scale=1.0") 6 | title= content_for?(:title) ? yield(:title) : "Snow Globe Theater" 7 | meta(name="description" 8 | content="#{content_for?(:description) ? yield(:description) : "Globe"}") 9 | = stylesheet_link_tag("application", 10 | media: "all", "data-turbolinks-track" => true) 11 | = javascript_include_tag("#{STRIPE_JS_HOST}/v2/#{STRIPE_JS_FILE}") 12 | = javascript_include_tag("application", "data-turbolinks-track" => true) 13 | 14 | javascript: 15 | Stripe.setPublishableKey( 16 | "#{Rails.application.secrets.stripe_publishable_key}"); 17 | 18 | = csrf_meta_tags 19 | 20 | body 21 | header 22 | = render "layouts/navigation" 23 | main(role="main") 24 | .container 25 | = render "layouts/messages" 26 | = yield 27 | -------------------------------------------------------------------------------- /app/workflows/cancels_stripe_subscription.rb: -------------------------------------------------------------------------------- 1 | class CancelsStripeSubscription 2 | 3 | attr_accessor :subscription_id, :user, :success 4 | 5 | def initialize(subscription_id:, user:) 6 | @subscription_id = subscription_id 7 | @user = user 8 | @success = false 9 | end 10 | 11 | def subscription 12 | @subcscription ||= Subscription.find_by(id: subscription_id) 13 | end 14 | 15 | def customer 16 | @customer ||= StripeCustomer.new(user) 17 | end 18 | 19 | def remote_subscription 20 | @remote_subscription ||= 21 | customer.subscriptions.retrieve(subscription.remote_id) 22 | end 23 | 24 | def user_is_subscribed? 25 | subscription_id && user.subscriptions.map(&:id).include?(subscription_id) 26 | end 27 | 28 | def run 29 | return unless user_is_subscribed? 30 | return if customer.nil? || remote_subscription.nil? 31 | remote_subscription.delete 32 | subscription.canceled! 33 | @success = true 34 | end 35 | end -------------------------------------------------------------------------------- /app/controllers/affiliates_controller.rb: -------------------------------------------------------------------------------- 1 | class AffiliatesController < ApplicationController 2 | 3 | def new 4 | end 5 | 6 | def show 7 | @affiliate = Affiliate.find(params[:id]) 8 | end 9 | 10 | def edit 11 | @affiliate = Affiliate.find(params[:id]) 12 | end 13 | 14 | def update 15 | @affiliate = Affiliate.find(params[:id]) 16 | workflow = UpdatesAffiliateAccount.new( 17 | affiliate: @affiliate, user: current_user, 18 | params: params[:account].permit!.to_h) 19 | workflow.run 20 | if workflow.success 21 | redirect_to user_path(current_user) 22 | else 23 | render :edit 24 | end 25 | end 26 | 27 | def create 28 | workflow = AddsAffiliateAccount.new( 29 | user: current_user, tos_checked: params[:tos], 30 | request_ip: request.remote_ip) 31 | workflow.run 32 | if workflow.success 33 | redirect_to user_path(current_user) 34 | else 35 | render :new 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /config/locales/simple_form.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | simple_form: 3 | "yes": 'Yes' 4 | "no": 'No' 5 | required: 6 | text: 'required' 7 | mark: '*' 8 | # You can uncomment the line below if you need to overwrite the whole required html. 9 | # When using html, text and mark won't be used. 10 | # html: '*' 11 | error_notification: 12 | default_message: "Please review the problems below:" 13 | # Examples 14 | # labels: 15 | # defaults: 16 | # password: 'Password' 17 | # user: 18 | # new: 19 | # email: 'E-mail to sign in.' 20 | # edit: 21 | # email: 'E-mail.' 22 | # hints: 23 | # defaults: 24 | # username: 'User name to sign in.' 25 | # password: 'No special characters, please.' 26 | # include_blanks: 27 | # defaults: 28 | # age: 'Rather not say' 29 | # prompts: 30 | # defaults: 31 | # age: 'Select your age' 32 | -------------------------------------------------------------------------------- /app/views/users/show.html.slim: -------------------------------------------------------------------------------- 1 | h1 User dashboard for #{current_user.email} 2 | 3 | h2 Subscriptions 4 | 5 | - current_user.subscriptions.each do |subscription| 6 | .subscription id=dom_id(subscription) 7 | - if subscription.status == "pending_initial_payment" || \ 8 | subscription.status == "active" 9 | p #{subscription.plan.nickname} ending #{subscription.end_date} 10 | .btn 11 | = link_to("Cancel Subscription", subscription, id: "cancel", 12 | method: :delete, data: { confirm: "Are you sure?" }) 13 | .btn 14 | = link_to("Change subscription plan", 15 | edit_subscription_path(subscription), 16 | id: "change_plan") 17 | 18 | h3 Affiliate 19 | 20 | - if current_user.affiliates.empty? 21 | = link_to "Make me an affiliate", new_affiliate_path 22 | - else 23 | h4 Affiliate Tags 24 | 25 | ul 26 | - current_user.affiliates.each do |affiliate| 27 | li= link_to(affiliate.tag, root_path(tag: affiliate.tag)) 28 | -------------------------------------------------------------------------------- /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 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /app/controllers/refunds_controller.rb: -------------------------------------------------------------------------------- 1 | class RefundsController < ApplicationController 2 | 3 | def create 4 | load_refundable 5 | authorize(@refundable, :refund?) 6 | workflow = PreparesStripeRefund.new( 7 | refundable: @refundable, 8 | administrator: current_user, 9 | refund_amount_cents: @refundable.price_cents) 10 | workflow.run 11 | if workflow.error 12 | flash[:error] = workflow.error 13 | else 14 | flash[:notice] = "Refund submitted" 15 | end 16 | redirect_to redirect_path 17 | end 18 | 19 | VALID_REFUNDABLES = %w(Payment PaymentLineItem).freeze 20 | 21 | private 22 | 23 | def load_refundable 24 | raise "bad refundable class" unless params[:type].in?(VALID_REFUNDABLES) 25 | @refundable = params[:type].constantize.find(params[:id]) 26 | end 27 | 28 | def redirect_path 29 | if params[:type] == "Payment" 30 | admin_payments_path 31 | else 32 | admin_payment_line_items_path 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /app/reports/daily_revenue.rb: -------------------------------------------------------------------------------- 1 | class DailyRevenue 2 | 3 | include Reportable 4 | 5 | attr_accessor :date, :revenue, :discounts 6 | 7 | def self.find_collection 8 | ActiveRecord::Base.connection.select_all( 9 | %{SELECT date(created_at) as date, 10 | sum(price_cents) as price_cents, 11 | sum(discount_cents) as discount_cents 12 | FROM "payments" 13 | WHERE "payments"."status" = 1 14 | GROUP BY date(created_at)}).map do |data| 15 | DailyRevenue.new(**data.symbolize_keys) 16 | end 17 | end 18 | 19 | def initialize(date:, price_cents:, discount_cents:) 20 | @date = date 21 | @revenue = price_cents.to_money 22 | @discounts = discount_cents.to_money 23 | end 24 | 25 | columns do 26 | column(:date) 27 | column(:revenue) 28 | column(:discounts) 29 | column(:ticket_count) 30 | end 31 | 32 | def ticket_count 33 | PaymentLineItem.tickets.no_refund. 34 | where("date(created_at) = ?", date).count 35 | end 36 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Take My Money: Accepting Payments on the Web 2 | ##### by Noel Rappin 3 | 4 | The code from "Take My Money" book, published by the Pragmatic Bookshelf. 5 | Copyrights apply to this code. It may not be used to create training material, courses, books, articles, and the like. Contact the publisher if you are in doubt. 6 | Visit http://www.pragmaticprogrammer.com/titles/nrwebpay for more book information. 7 | 8 | Completed. Chapters 13/13 9 | 10 | **Versions used:** 11 | 12 | * Ruby: 2.5.0 13 | * Rails: 5.1.4 14 | * PostgreSQL: 10.1 15 | * RSpec: 3.7 16 | 17 | **Configuration:** 18 | 19 | * Create database: 20 | ``` 21 | bundle exec rails db:create 22 | ``` 23 | * Run migrations: 24 | ``` 25 | bundle exec rails db:migrate 26 | ``` 27 | * Install ruby gems: 28 | ``` 29 | bundle install 30 | ``` 31 | 32 | **Testing:** 33 | 34 | * Using RSpec testing library: 35 | 36 | * unit tests 37 | 38 | * integration tests using Capybara 39 | ``` 40 | bundle exec rspec 41 | ```` -------------------------------------------------------------------------------- /app/controllers/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionsController < ApplicationController 2 | 3 | before_action :authenticate_user! 4 | 5 | def edit 6 | @subscription = Subscription.find(params[:id]) 7 | end 8 | 9 | def update 10 | subscription = Subscription.find(params[:id]) 11 | workflow = ChangesStripeSubscriptionPlan.new( 12 | subscription_id: subscription.id, 13 | user: current_user, 14 | new_plan_id: params[:new_plan]) 15 | workflow.run 16 | if workflow.success 17 | redirect_to user_path(current_user), 18 | notice: "Subscription plan was successfully changed" 19 | end 20 | end 21 | 22 | def destroy 23 | subscription = Subscription.find(params[:id]) 24 | workflow = CancelsStripeSubscription.new( 25 | subscription_id: subscription.id, 26 | user: current_user) 27 | workflow.run 28 | if workflow.success 29 | redirect_to user_path(current_user), 30 | notice: "Subscription was successfully canceled" 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /app/admin/dashboard.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register_page "Dashboard" do 2 | 3 | menu priority: 1, label: proc{ I18n.t("active_admin.dashboard") } 4 | 5 | content title: proc{ I18n.t("active_admin.dashboard") } do 6 | div class: "blank_slate_container", id: "dashboard_default_message" do 7 | span class: "blank_slate" do 8 | span I18n.t("active_admin.dashboard_welcome.welcome") 9 | small I18n.t("active_admin.dashboard_welcome.call_to_action") 10 | end 11 | end 12 | 13 | # Here is an example of a simple dashboard with columns and panels. 14 | # 15 | # columns do 16 | # column do 17 | # panel "Recent Posts" do 18 | # ul do 19 | # Post.recent(5).map do |post| 20 | # li link_to(post.title, admin_post_path(post)) 21 | # end 22 | # end 23 | # end 24 | # end 25 | 26 | # column do 27 | # panel "Info" do 28 | # para "Welcome to ActiveAdmin." 29 | # end 30 | # end 31 | # end 32 | end # content 33 | end 34 | -------------------------------------------------------------------------------- /app/workflows/prepares_cart_for_stripe.rb: -------------------------------------------------------------------------------- 1 | class PreparesCartForStripe < PreparesCart 2 | 3 | attr_accessor :stripe_token, :stripe_charge 4 | 5 | def initialize(user:, stripe_token:, purchase_amount_cents:, 6 | expected_ticket_ids:, payment_reference: nil, shopping_cart:) 7 | super(user: user, purchase_amount_cents: purchase_amount_cents, 8 | expected_ticket_ids: expected_ticket_ids, 9 | payment_reference: payment_reference, 10 | shopping_cart: shopping_cart) 11 | @stripe_token = stripe_token 12 | end 13 | 14 | def update_tickets 15 | tickets.each(&:purchased!) 16 | end 17 | 18 | def on_success 19 | ExecutesStripePaymentJob.perform_later(payment, stripe_token.id) 20 | end 21 | 22 | def payment_attributes 23 | result = super.merge(payment_method: "stripe") 24 | if shopping_cart.affiliate 25 | result = result.merge( 26 | affiliate_id: shopping_cart.affiliate.id, 27 | affiliate_payment_cents: price_calculator.affiliate_payment.cents) 28 | end 29 | result 30 | end 31 | end -------------------------------------------------------------------------------- /spec/features/users/user_edit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.feature "User edit", :devise do 4 | scenario "user changes email address" do 5 | user = FactoryBot.create(:user) 6 | sign_in(user.email, user.password) 7 | 8 | visit edit_user_registration_path(user) 9 | fill_in "Email", with: "newmail@example.com" 10 | fill_in "Current password", with: user.password 11 | click_button "Update" 12 | 13 | txts = [I18n.t("devise.registrations.updated"), 14 | I18n.t("devise.registrations.update_needs_confirmation")] 15 | expect(page).to have_content(/.*#{txts[0]}.*|.*#{txts[1]}.*/) 16 | end 17 | 18 | scenario "user cannot edit another user's profile" do 19 | user = FactoryBot.create(:user) 20 | another_user = FactoryBot.create(:user, email: "other@example.com") 21 | 22 | sign_in(user.email, user.password) 23 | visit edit_user_registration_path(another_user) 24 | 25 | expect(page).to have_content("Edit User") 26 | expect(page).to have_field("Email", with: user.email) 27 | end 28 | end -------------------------------------------------------------------------------- /app/admin/tickets.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register Ticket do 2 | permit_params :performance_id, :status, :access, :price_cents, :price_currency 3 | 4 | filter :performance 5 | filter :event 6 | filter :status 7 | filter :access 8 | 9 | index do 10 | selectable_column 11 | id_column 12 | column :user_id 13 | column :status 14 | column :access 15 | column :price 16 | column :payment_reference 17 | actions 18 | end 19 | 20 | show do 21 | attributes_table do 22 | row :user_id 23 | row :performance 24 | row :status 25 | row :access 26 | row :price_cents 27 | row :price_currency 28 | row :created_at 29 | row :updated_at 30 | row :payment_reference 31 | end 32 | active_admin_comments 33 | end 34 | 35 | form do |f| 36 | f.semantic_errors(*f.object.errors.keys) 37 | f.inputs do 38 | f.input :performance 39 | f.input :status 40 | f.input :access 41 | f.input :price_cents 42 | f.input :price_currency 43 | end 44 | f.actions 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/workflows/creates_plan.rb: -------------------------------------------------------------------------------- 1 | class CreatesPlan 2 | 3 | attr_accessor :remote_id, :product, :nickname, :price_cents, :interval, 4 | :tickets_allowed, :ticket_category, :plan 5 | 6 | def initialize(remote_id:, product:, nickname:, price_cents:, interval:, 7 | interval_count:, tickets_allowed:, ticket_category:) 8 | @remote_id = remote_id 9 | @product = product 10 | @nickname = nickname 11 | @price_cents = price_cents 12 | @interval = interval 13 | @interval_count = interval_count 14 | @tickets_allowed = tickets_allowed 15 | @ticket_category = ticket_category 16 | end 17 | 18 | def run 19 | remote_plan = Stripe::Plan.create( 20 | id: remote_id, product: product, amount: price_cents, currency: "usd", 21 | interval: interval, interval_count: 1, nickname: nickname) 22 | self.plan = Plan.create( 23 | remote_id: remote_plan.id, nickname: nickname, price_cents: price_cents, 24 | interval: interval, interval_count: 1, tickets_allowed: tickets_allowed, 25 | ticket_category: ticket_category, status: :active) 26 | end 27 | end -------------------------------------------------------------------------------- /spec/features/subscriptions/user_purchases_subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "User purchases a subscription plan", :vcr do 4 | let(:user) { create(:user) } 5 | let(:plan) { create(:plan, remote_id: "orchestra_monthly", 6 | nickname: "Orchestra Monthly") } 7 | let!(:subscription) { Subscription.create(user: user, plan: plan, 8 | start_date: Time.zone.now.to_date, end_date: plan.end_date_from, 9 | status: :waiting) } 10 | let(:token) { StripeToken.new( 11 | credit_card_number: "4242424242424242", expiration_month: "12", 12 | expiration_year: Time.zone.now.year + 1, cvc: "123") } 13 | 14 | before(:example) do 15 | sign_in(user.email, user.password) 16 | end 17 | 18 | scenario "via stripe" do 19 | visit subscription_cart_path 20 | 21 | choose "credit_radio" 22 | find("#spec_stripe_token", visible: false).set(token.id) 23 | click_on "purchase" 24 | user.reload 25 | subscription.reload 26 | 27 | expect(user.stripe_id).not_to be_nil 28 | expect(subscription).to be_pending_initial_payment 29 | end 30 | end -------------------------------------------------------------------------------- /app/models/stripe_customer.rb: -------------------------------------------------------------------------------- 1 | class StripeCustomer 2 | 3 | attr_accessor :user 4 | 5 | delegate :subscriptions, :id, to: :remote_customer 6 | 7 | def initialize(user) 8 | @user = user 9 | end 10 | 11 | def remote_customer 12 | @remote_customer ||= begin 13 | if user.stripe_id 14 | Stripe::Customer.retrieve(user.stripe_id) 15 | else 16 | Stripe::Customer.create(email: user.email).tap do |remote_customer| 17 | user.update!(stripe_id: remote_customer.id) 18 | end 19 | end 20 | end 21 | end 22 | 23 | def valid? 24 | remote_customer.present? 25 | end 26 | 27 | def find_subscription_for(plan) 28 | subscriptions.find { |s| s.plan.id == plan.remote_id } 29 | end 30 | 31 | def add_subscription(subscription) 32 | remote_subscription = remote_customer.subscriptions.create( 33 | plan: subscription.remote_plan_id) 34 | subscription.update!(remote_id: remote_subscription.id) 35 | end 36 | 37 | def source=(token) 38 | remote_customer.source = token.id 39 | remote_customer.save 40 | end 41 | end -------------------------------------------------------------------------------- /app/workflows/executes_stripe_payment.rb: -------------------------------------------------------------------------------- 1 | class ExecutesStripePayment 2 | 3 | attr_accessor :payment, :stripe_token, :stripe_charge 4 | 5 | def initialize(payment:, stripe_token:) 6 | @payment = payment 7 | @stripe_token = StripeToken.new(stripe_token: stripe_token) 8 | end 9 | 10 | def run 11 | Payment.transaction do 12 | result = charge 13 | result ? on_success : on_failure 14 | end 15 | end 16 | 17 | def on_success 18 | PaymentMailer.notify_success(payment).deliver_later 19 | NotifyTaxCloudJob.perform_later(payment) 20 | end 21 | 22 | def on_failure 23 | unpurchase_tickets 24 | PaymentMailer.notify_failure(payment).deliver_later 25 | end 26 | 27 | def charge 28 | raise PreExistingPaymentException if payment.response_id.present? 29 | @stripe_charge = StripeCharge.new(token: stripe_token, payment: payment) 30 | @stripe_charge.charge 31 | payment.update!(@stripe_charge.payment_attributes) 32 | payment.succeeded? 33 | end 34 | 35 | def unpurchase_tickets 36 | payment.tickets.each(&:waiting!) 37 | end 38 | end -------------------------------------------------------------------------------- /app/models/stripe_token.rb: -------------------------------------------------------------------------------- 1 | class StripeToken 2 | 3 | attr_accessor :credit_card_number, :expiration_month, 4 | :expiration_year, :cvc, 5 | :stripe_token 6 | 7 | def initialize(credit_card_number: nil, expiration_month: nil, 8 | expiration_year: nil, cvc: nil, 9 | stripe_token: nil) 10 | @credit_card_number = credit_card_number 11 | @expiration_month = expiration_month 12 | @expiration_year = expiration_year 13 | @cvc = cvc 14 | @stripe_token = stripe_token 15 | end 16 | 17 | def token 18 | @token ||= (stripe_token ? retrieve_token : create_token) 19 | end 20 | 21 | def id 22 | stripe_token || token.id 23 | end 24 | 25 | private def retrieve_token 26 | Stripe::Token.retrieve(stripe_token) 27 | end 28 | 29 | private def create_token 30 | Stripe::Token.create( 31 | card: {number: credit_card_number, exp_month: expiration_month, 32 | exp_year: expiration_year, cvc: cvc}) 33 | end 34 | 35 | def to_s 36 | "STRIPE TOKEN: #{id}" 37 | end 38 | 39 | def inspect 40 | "STRIPE TOKEN: #{id}" 41 | end 42 | end -------------------------------------------------------------------------------- /spec/workflows/creates_subscription_via_stripe_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe CreatesSubscriptionViaStripe, :vcr, :aggregate_failures do 4 | describe "happy path" do 5 | let(:user) { create(:user) } 6 | let(:plan) { create(:plan, remote_id: "orchestra_monthly", 7 | nickname: "Orchestra Monthly") } 8 | let(:subscription) { Subscription.create!( 9 | user: user, plan: plan, status: :waiting) } 10 | let(:token) { StripeToken.new( 11 | credit_card_number: "4242424242424242", expiration_month: "12", 12 | expiration_year: Time.zone.now.year + 1, cvc: "123") } 13 | let(:workflow) { CreatesSubscriptionViaStripe.new( 14 | user: user, expected_subscription_id: [subscription.id], token: token) } 15 | 16 | 17 | it "creates a customer" do 18 | workflow.run 19 | subscription.reload 20 | 21 | expect(subscription).to be_pending_initial_payment 22 | expect(user.stripe_id).to be_present 23 | expect(subscription.payment_method).to eq("stripe") 24 | expect(subscription.remote_id).to be_present 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe UsersController, :aggregate_failures do 4 | let(:logged_in_user) { create(:user) } 5 | let(:other_user) { create(:user, email: "other@example.com") } 6 | 7 | describe "access to show and edit" do 8 | 9 | before(:example) do 10 | sign_in(logged_in_user) 11 | end 12 | 13 | it "allows a user to view their own page" do 14 | get :show, params: {id: logged_in_user} 15 | 16 | expect(response).to be_a_success 17 | end 18 | 19 | it "blocks a user from viewing another user's page" do 20 | get :show, params: {id: other_user} 21 | 22 | expect(response).to be_forbidden 23 | expect(controller.user_signed_in?).to be_falsy 24 | end 25 | 26 | context "logging in as an admin" do 27 | 28 | before(:example) { logged_in_user.admin! } 29 | 30 | it "allows an admin to view another user's page" do 31 | get :show, params: {id: other_user} 32 | 33 | expect(response).to be_a_success 34 | end 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /spec/workflows/cancels_stripe_subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe CancelsStripeSubscription, :vcr, :aggregate_failures do 4 | describe "happy path" do 5 | let(:user) { create(:user) } 6 | let(:plan) { create(:plan, remote_id: "orchestra_monthly", 7 | nickname: "Orchestra Monthly") } 8 | let(:subscription) { Subscription.create!( 9 | user: user, plan: plan, status: :waiting) } 10 | let(:token) { StripeToken.new( 11 | credit_card_number: "4242424242424242", expiration_month: "12", 12 | expiration_year: Time.zone.now.year + 1, cvc: "123") } 13 | let(:workflow) { CreatesSubscriptionViaStripe.new( 14 | user: user, expected_subscription_id: [subscription.id], token: token) } 15 | 16 | before(:example) do 17 | workflow.run 18 | end 19 | 20 | it "cancels stripe subscription" do 21 | action = CancelsStripeSubscription.new( 22 | subscription_id: subscription.id, user: user) 23 | 24 | action.run 25 | subscription.reload 26 | 27 | expect(subscription).to be_canceled 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /app/workflows/creates_subscription_via_stripe.rb: -------------------------------------------------------------------------------- 1 | class CreatesSubscriptionViaStripe 2 | 3 | attr_accessor :user, :expected_subscription_id, :token, :success 4 | 5 | def initialize(user:, expected_subscription_id:, token:) 6 | @user = user 7 | @expected_subscription_id = expected_subscription_id 8 | @token = token 9 | @success = false 10 | end 11 | 12 | def subscription 13 | @subscription ||= user.subscriptions_in_cart.first 14 | end 15 | 16 | def expected_plan_valid? 17 | expected_subscription_id.first.to_i == subscription.id.to_i 18 | end 19 | 20 | def run 21 | Payment.transaction do 22 | return unless expected_plan_valid? 23 | stripe_customer = StripeCustomer.new(user) 24 | return unless stripe_customer.valid? 25 | stripe_customer.source = token 26 | subscription.make_stripe_payment(stripe_customer) 27 | stripe_customer.add_subscription(subscription) 28 | @success = true 29 | end 30 | rescue Stripe::StripeError => exception 31 | Rollbar.error(exception) 32 | end 33 | 34 | def redirect_on_success_url 35 | user 36 | end 37 | end -------------------------------------------------------------------------------- /app/admin/subscriptions.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register Subscription do 2 | filter :plan_id 3 | filter :start_date 4 | filter :end_date 5 | filter :status 6 | filter :payment_method 7 | filter :created_at 8 | 9 | index do 10 | selectable_column 11 | id_column 12 | column :user_id 13 | column :plan 14 | column :start_date 15 | column :end_date 16 | column :status 17 | column :payment_method 18 | column :remote_id 19 | column :created_at 20 | actions 21 | end 22 | 23 | show do 24 | attributes_table do 25 | row :user_id 26 | row :plan 27 | row :start_date 28 | row :end_date 29 | row :status 30 | row :payment_method 31 | row :remote_id 32 | row :created_at 33 | row :updated_at 34 | end 35 | active_admin_comments 36 | end 37 | 38 | form do |f| 39 | f.inputs do 40 | f.semantic_errors(*f.object.errors.keys) 41 | f.input :user_id 42 | f.input :plan 43 | f.input :start_date, as: :string 44 | f.input :end_date, as: :string 45 | f.input :status 46 | end 47 | f.actions 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /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 | # Install JavaScript dependencies if using Yarn 22 | # system('bin/yarn') 23 | 24 | 25 | # puts "\n== Copying sample files ==" 26 | # unless File.exist?('config/database.yml') 27 | # cp 'config/database.yml.sample', 'config/database.yml' 28 | # end 29 | 30 | puts "\n== Preparing database ==" 31 | system! 'bin/rails db:setup' 32 | 33 | puts "\n== Removing old logs and tempfiles ==" 34 | system! 'bin/rails log:clear tmp:clear' 35 | 36 | puts "\n== Restarting application server ==" 37 | system! 'bin/rails restart' 38 | end 39 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | require "sprockets/railtie" 13 | # require "rails/test_unit/railtie" 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module SnowGlobe 20 | class Application < Rails::Application 21 | # Initialize configuration defaults for originally generated Rails version. 22 | config.load_defaults 5.1 23 | 24 | # Settings in config/environments/* take precedence over those specified here. 25 | # Application configuration should go into files in config/initializers 26 | # -- all .rb files in that directory are automatically loaded. 27 | 28 | # Don't generate system test files. 29 | config.generators.system_tests = nil 30 | config.active_job.queue_adapter = :delayed_job 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/reports/day_revenue_report_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe DayRevenueReport, type: :model do 4 | let!(:really_old_payment) { create( 5 | :payment, created_at: 1.month.ago, price_cents: 4500) } 6 | let!(:really_old_payment_2) { create( 7 | :payment, created_at: 1.month.ago, price_cents: 1500) } 8 | let!(:old_payment) { create( 9 | :payment, created_at: 2.days.ago, price_cents: 3500) } 10 | let!(:yesterday_payment) { create( 11 | :payment, created_at: 1.day.ago, price_cents: 2500) } 12 | let!(:now_payment) { create( 13 | :payment, created_at: 1.second.ago, price_cents: 1500) } 14 | 15 | before(:example) do 16 | BuildDayRevenueJob.perform_now 17 | end 18 | 19 | it "generates the expected report" do 20 | enum = DayRevenueReport.to_csv_enumerator 21 | 22 | expect(enum.next).to eq("Day,Price,Discounts,Ticket count\n") 23 | expect(enum.next).to eq("#{1.month.ago.to_date},60.00,0.00,0\n") 24 | expect(enum.next).to eq("#{2.days.ago.to_date},35.00,0.00,0\n") 25 | expect(enum.next).to eq("#{1.day.ago.to_date},25.00,0.00,0\n") 26 | expect(enum.next).to eq("#{Date.current},15.00,0.00,0\n") 27 | end 28 | end -------------------------------------------------------------------------------- /spec/features/subscriptions/user_cancels_subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "User cancels subscription", :vcr do 4 | let(:user) { create(:user) } 5 | let(:plan) { create(:plan, remote_id: "orchestra_monthly", 6 | nickname: "Orchestra Monthly") } 7 | let!(:subscription) { Subscription.create(user: user, plan: plan, 8 | start_date: Time.zone.now.to_date, end_date: plan.end_date_from, 9 | status: :waiting) } 10 | let(:token) { StripeToken.new( 11 | credit_card_number: "4242424242424242", expiration_month: "12", 12 | expiration_year: Time.zone.now.year + 1, cvc: "123") } 13 | let(:workflow) { CreatesSubscriptionViaStripe.new( 14 | user: user, expected_subscription_id: [subscription.id], token: token) } 15 | 16 | before(:example) do 17 | sign_in(user.email, user.password) 18 | workflow.run 19 | end 20 | 21 | scenario "via stripe" do 22 | visit user_path(user) 23 | 24 | within("#subscription_#{subscription.id}") do 25 | click_on("cancel") 26 | end 27 | 28 | expect(current_url).to match(user_path(user)) 29 | expect(page).to have_content("Subscription was successfully canceled") 30 | end 31 | end -------------------------------------------------------------------------------- /app/workflows/changes_stripe_subscription_plan.rb: -------------------------------------------------------------------------------- 1 | class ChangesStripeSubscriptionPlan 2 | 3 | attr_accessor :subscription_id, :user, :new_plan_id, :success 4 | 5 | def initialize(subscription_id:, user:, new_plan_id:) 6 | @subscription_id = subscription_id 7 | @user = user 8 | @new_plan_id = new_plan_id 9 | @success = false 10 | end 11 | 12 | def new_plan 13 | @plan ||= Plan.find_by(id: new_plan_id) 14 | end 15 | 16 | def subscription 17 | @subscription ||= Subscription.find_by(id: subscription_id) 18 | end 19 | 20 | def customer 21 | @customer ||= StripeCustomer.new(user) 22 | end 23 | 24 | def remote_subscription 25 | @remote_subscription ||= 26 | customer.subscriptions.retrieve(subscription.remote_id) 27 | end 28 | 29 | def user_is_subscribed? 30 | subscription_id && user.subscriptions.map(&:id).include?(subscription_id) 31 | end 32 | 33 | def run 34 | return unless user_is_subscribed? 35 | return if customer.nil? || remote_subscription.nil? 36 | remote_subscription.plan = new_plan.remote_id 37 | subscription.update(plan: new_plan) 38 | remote_subscription.save 39 | @success = true 40 | end 41 | end -------------------------------------------------------------------------------- /spec/features/users/sign_in_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.feature "Sign in", :devise do 4 | scenario "user cannot sign in if not registered" do 5 | sign_in("test@example.com", "password") 6 | 7 | expect(page).to have_content( 8 | I18n.t("devise.failure.not_found_in_database", 9 | authentication_keys: "Email")) 10 | end 11 | 12 | scenario "user can sign in with valid credentials" do 13 | user = FactoryBot.create(:user) 14 | 15 | sign_in(user.email, user.password) 16 | 17 | expect(page).to have_content(I18n.t("devise.sessions.signed_in")) 18 | end 19 | 20 | scenario "user cannot sign in with wrong email" do 21 | user = FactoryBot.create(:user) 22 | 23 | sign_in("invalid@email.com", user.password) 24 | 25 | expect(page).to have_content( 26 | I18n.t("devise.failure.not_found_in_database", 27 | authentication_keys: "Email")) 28 | end 29 | 30 | scenario "user cannot sign in with wrong password" do 31 | user = FactoryBot.create(:user) 32 | 33 | sign_in(user.email, "1234") 34 | 35 | expect(page).to have_content( 36 | I18n.t("devise.failure.invalid", authentication_keys: "Email")) 37 | end 38 | end -------------------------------------------------------------------------------- /app/views/shopping_carts/show.html.slim: -------------------------------------------------------------------------------- 1 | h1 Shopping Cart 2 | 3 | - @cart.events.each do |event| 4 | h2= event.name 5 | div(id=dom_id(event)) 6 | table.table 7 | thead 8 | th Date 9 | th Tickets 10 | th Total Price 11 | tbody 12 | - @cart.performances_for(event).each do |performance| 13 | tr(id=dom_id(performance)) 14 | td= performance.start_time.to_date.to_s(:long) 15 | td.ticket_count= @cart.performance_count[performance.id] 16 | td.subtotal 17 | = humanized_money_with_symbol(@cart.subtotal_for(performance)) 18 | 19 | - if @cart.discount_code 20 | h4.active_code Active Discount Code: #{@cart.discount_code.code} 21 | 22 | - if @cart.processing_fee 23 | h4.active_code 24 | | Processing Fee: #{humanized_money_with_symbol(@cart.processing_fee)} 25 | 26 | - if @cart.shipping_fee 27 | h4.active_code 28 | | Shipping Fee: #{humanized_money_with_symbol(@cart.shipping_fee)} 29 | 30 | - if @cart.sales_tax.nonzero? 31 | h4.active_code 32 | | Sales Tax: #{humanized_money_with_symbol(@cart.sales_tax)} 33 | 34 | h3.total Total #{humanized_money_with_symbol(@cart.total_cost)} 35 | 36 | h2 Checkout 37 | 38 | = render "credit_card_info" 39 | -------------------------------------------------------------------------------- /spec/reports/report_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe ReportBuilder do 4 | let(:data_one) { OpenStruct.new(first_name: "Alex", last_name: "Khlipun") } 5 | let(:data_two) { OpenStruct.new(first_name: "Noel", last_name: "Rappin") } 6 | let(:builder) { ReportBuilder.new do 7 | column(:first_name) 8 | column(:last_name) 9 | end } 10 | 11 | it "converts data into an array of hashes" do 12 | result = builder.build([data_one, data_two]) 13 | 14 | expect(result).to eq( 15 | [{"First name" => "Alex", "Last name" => "Khlipun"}, 16 | {"First name" => "Noel", "Last name" => "Rappin"}]) 17 | end 18 | 19 | it "converts data into an array of hashes without friendly names" do 20 | builder.humanize_name = false 21 | result = builder.build([data_one, data_two]) 22 | 23 | expect(result).to eq( 24 | [{"first_name" => "Alex", "last_name" => "Khlipun"}, 25 | {"first_name" => "Noel", "last_name" => "Rappin"}]) 26 | end 27 | 28 | it "handles CSV" do 29 | result = builder.build([data_one, data_two], format: :csv) 30 | 31 | expect(result).to eq("First name,Last name\nAlex,Khlipun\nNoel,Rappin\n") 32 | end 33 | end -------------------------------------------------------------------------------- /app/workflows/executes_pay_pal_payment.rb: -------------------------------------------------------------------------------- 1 | class ExecutesPayPalPayment 2 | 3 | attr_accessor :payment_id, :token, :payer_id, :payment, :success 4 | 5 | def initialize(payment_id:, token:, payer_id:) 6 | @payment_id = payment_id 7 | @token = token 8 | @payer_id = payer_id 9 | @success = false 10 | @continue = true 11 | end 12 | 13 | def payment 14 | @payment ||= Payment.find_by( 15 | payment_method: "paypal", response_id: payment_id) 16 | end 17 | 18 | def pay_pal_payment 19 | @pay_pal_payment ||= PayPalPayment.find(payment_id) 20 | end 21 | 22 | def run 23 | Payment.transaction do 24 | pre_purchase 25 | purchase 26 | post_purchase 27 | end 28 | end 29 | 30 | def pre_purchase 31 | @continue = pay_pal_payment.valid? 32 | end 33 | 34 | def purchase 35 | return unless @continue 36 | @continue = pay_pal_payment.execute(payer_id: payer_id) 37 | end 38 | 39 | def post_purchase 40 | if @continue 41 | payment.tickets.each(&:purchased!) 42 | payment.succeeded! 43 | self.success = true 44 | NotifyTaxCloudJob.perform_later(payment) 45 | else 46 | payment.tickets.each(&:waiting!) 47 | payment.failed! 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /app/models/stripe_refund.rb: -------------------------------------------------------------------------------- 1 | class StripeRefund 2 | 3 | attr_accessor :payment_to_refund, :response, :error, :amount_to_refund 4 | 5 | def initialize(payment_to_refund:, amount_to_refund: nil) 6 | @payment_to_refund = payment_to_refund 7 | @amount_to_refund = amount_to_refund || -payment_to_refund.price_cents 8 | end 9 | 10 | delegate :original_payment, to: :payment_to_refund 11 | 12 | def refund 13 | return if original_payment.nil? 14 | @response = Stripe::Refund.create( 15 | {charge: original_payment.response_id, amount: amount_to_refund, 16 | metadata: { 17 | refund_reference: payment_to_refund.reference, 18 | original_reference: original_payment.reference}}, 19 | idempotency_key: payment_to_refund.reference) 20 | rescue Stripe::CardError => e 21 | @response = nil 22 | @error = e 23 | end 24 | 25 | def success? 26 | response || !error 27 | end 28 | 29 | def refund_attributes 30 | success? ? success_attributes : failure_attributes 31 | end 32 | 33 | def success_attributes 34 | {status: :refunded, 35 | response_id: response.id, full_response: response.to_json} 36 | end 37 | 38 | def failure_attributes 39 | {status: :failed, full_response: error.to_json} 40 | end 41 | end -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | 3 | protect_from_forgery with: :exception 4 | 5 | before_action :set_paper_trail_whodunnit 6 | before_action :set_affiliate 7 | 8 | include Pundit 9 | 10 | rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized 11 | 12 | def current_user 13 | return nil if session[:awaiting_authy_user_id].present? 14 | super 15 | end 16 | 17 | def current_cart 18 | ShoppingCart.for(user: current_user) 19 | end 20 | 21 | def user_for_paper_trail 22 | simulating_admin_user || current_user 23 | end 24 | 25 | def simulating_admin_user 26 | User.find_by(id: session[:admin_id]) 27 | end 28 | helper_method :simulating_admin_user 29 | 30 | def authenticate_admin_user! 31 | raise Pundit::NotAuthorizedError unless current_user&.admin? 32 | end 33 | 34 | def set_affiliate 35 | tag = params[:tag] || session[:affiliate_tag] 36 | workflow = AddsAffiliateToCart.new(user: current_user, tag: tag) 37 | workflow.run 38 | session[:affiliate_tag] = tag 39 | end 40 | 41 | private 42 | 43 | def user_not_authorized 44 | sign_out(User) 45 | render plain: "Access Not Allowed", status: :forbidden 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/controllers/users/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::SessionsController < Devise::SessionsController 2 | 3 | skip_before_action :set_paper_trail_whodunnit 4 | 5 | def create 6 | @user = User.find_by(email: params[:user][:email]) 7 | if @user&.authy_id.present? 8 | if @user&.valid_password?(params[:user][:password]) 9 | session[:awaiting_authy_user_id] = @user.id 10 | Authy::API.request_sms(id: @user.authy_id) 11 | render :two_factor 12 | else 13 | render :new 14 | end 15 | else 16 | session[:awaiting_authy_user_id] = nil 17 | super 18 | end 19 | end 20 | 21 | def two_factor 22 | end 23 | 24 | def verify 25 | @user = User.find(session[:awaiting_authy_user_id]) 26 | token = Authy::API.verify(id: @user.authy_id, token: params[:token]) 27 | if token.ok? 28 | set_flash_message!(:notice, :signed_in) 29 | sign_in(User, @user) 30 | session[:awaiting_authy_user_id] = nil 31 | respond_with @user, location: after_sign_in_path_for(resource) 32 | else 33 | flash[:danger] = "Incorrect code, please try again" 34 | redirect_to users_sessions_two_factor_path 35 | end 36 | end 37 | 38 | def delete 39 | session[:awaiting_authy_user_id] = nil 40 | super 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/workflows/changes_stripe_subscription_plan_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe ChangesStripeSubscriptionPlan, :vcr, :aggregate_failures do 4 | describe "happy path" do 5 | let(:user) { create(:user) } 6 | let(:plan) { create(:plan, remote_id: "orchestra_monthly", 7 | nickname: "Orchestra Monthly") } 8 | let(:new_plan) { create(:plan, remote_id: "balcony_monthly", 9 | nickname: "Balcony Monthly") } 10 | let(:subscription) { Subscription.create!( 11 | user: user, plan: plan, status: :waiting) } 12 | let(:token) { StripeToken.new( 13 | credit_card_number: "4242424242424242", expiration_month: "12", 14 | expiration_year: Time.zone.now.year + 1, cvc: "123") } 15 | let(:workflow) { CreatesSubscriptionViaStripe.new( 16 | user: user, expected_subscription_id: [subscription.id], token: token) } 17 | 18 | before(:example) do 19 | workflow.run 20 | end 21 | 22 | it "changes stripe subscription plan" do 23 | action = ChangesStripeSubscriptionPlan.new( 24 | subscription_id: subscription.id, user: user, 25 | new_plan_id: new_plan.id) 26 | 27 | action.run 28 | subscription.reload 29 | 30 | expect(subscription.plan.nickname).to eq("Balcony Monthly") 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /app/views/addresses/new.html.slim: -------------------------------------------------------------------------------- 1 | h3 Shipping Address 2 | = form_for(@address) do |f| 3 | .row 4 | .form-group 5 | .col-sm-2 6 | = label_tag(:shipping_method) 7 | .col-sm-2 8 | = select_tag("shipping_method", 9 | options_for_select(mailer_options), class: "form-control") 10 | .row 11 | .form-group 12 | .col-sm-2 13 | = f.label(:address_1, "Address Line 1", class: "control-label") 14 | .col-sm-3 15 | = f.text_field("address_1", class: "form-control") 16 | .row 17 | .form-group 18 | .col-sm-2 19 | = f.label(:address_2, "Address Line 2", class: "control-label") 20 | .col-sm-3 21 | = f.text_field("address_2", class: "form-control") 22 | 23 | .row 24 | .form-group 25 | .col-sm-2 26 | = f.label(:city, "City/State/Zip", class: "control-label") 27 | .col-sm-3 28 | = f.text_field("city", class: "form-control") 29 | .col-sm-2 30 | = f.select("state", 31 | options_for_select(CS.states(:us).invert), {}, 32 | class: "form-control") 33 | .col-sm-2 34 | = f.text_field("zip", class: "form-control", placeholder: "Zip") 35 | 36 | .row 37 | .form-group 38 | .col-sm-3 39 | = submit_tag("Add Address", class: "btn btn-default", id: "add_address") 40 | -------------------------------------------------------------------------------- /app/workflows/stripe_handler/invoice_payment_succeeded.rb: -------------------------------------------------------------------------------- 1 | module StripeHandler 2 | 3 | class InvoicePaymentSucceeded 4 | 5 | attr_accessor :event, :success, :payment 6 | 7 | def initialize(event) 8 | @event = event 9 | @success = false 10 | end 11 | 12 | def run 13 | Subscription.transaction do 14 | return unless event 15 | subscription.active! 16 | subscription.update_end_date 17 | @payment = Payment.create!( 18 | user_id: user.id, price_cents: invoice.amount_due, 19 | status: "succeeded", reference: Payment.generate_reference, 20 | payment_method: "stripe", response_id: invoice.charge, 21 | full_response: charge.to_json) 22 | payment.payment_line_items.create!( 23 | buyable: subscription, price_cents: invoice.amount_due) 24 | @success = true 25 | end 26 | end 27 | 28 | def invoice 29 | @event.data.object 30 | end 31 | 32 | def subscription 33 | @subscription ||= Subscription.find_by(remote_id: invoice.subscription) 34 | end 35 | 36 | def user 37 | @user ||= User.find_by(stripe_id: invoice.customer) 38 | end 39 | 40 | def charge 41 | @charge ||= Stripe::Charge.retrieve(invoice.charge) 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /app/jobs/prepares_cart_for_stripe_job.rb: -------------------------------------------------------------------------------- 1 | class PreparesCartForStripeJob < ApplicationJob 2 | 3 | queue_as :default 4 | 5 | rescue_from(ChargeSetupValidityException) do |exception| 6 | PaymentMailer.notify_failure(exception).deliver_later 7 | Rollbar.error(exception) 8 | end 9 | 10 | rescue_from(PreExistingPaymentException) do |exception| 11 | Rollbar.error(exception) 12 | end 13 | 14 | def perform(user:, purchase_amount_cents:, expected_ticket_ids:, 15 | payment_reference:, params:, shopping_cart:) 16 | token = StripeToken.new(**card_params(params)) 17 | user.tickets_in_cart.each do |ticket| 18 | ticket.update(payment_reference: payment_reference) 19 | end 20 | purchases_cart_workflow = PreparesCartForStripe.new( 21 | user: user, stripe_token: token, 22 | purchase_amount_cents: purchase_amount_cents, 23 | expected_ticket_ids: expected_ticket_ids, 24 | payment_reference: payment_reference, 25 | shopping_cart: shopping_cart) 26 | purchases_cart_workflow.run 27 | end 28 | 29 | def success 30 | true 31 | end 32 | 33 | def redirect_on_success_url 34 | nil 35 | end 36 | 37 | private def card_params(params) 38 | params.slice( 39 | :credit_card_number, :expiration_month, 40 | :expiration_year, :cvc, 41 | :stripe_token).symbolize_keys 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/admin/payments.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register Payment do 2 | actions :all, except: [:new, :edit] 3 | 4 | filter :reference 5 | filter :price_cents 6 | filter :status 7 | filter :payment_method 8 | filter :created_at 9 | 10 | index do 11 | selectable_column 12 | id_column 13 | column :reference 14 | column :user_id 15 | column :price 16 | column :status 17 | column :payment_method 18 | column :created_at 19 | actions 20 | end 21 | 22 | show do 23 | attributes_table do 24 | row :reference 25 | row :price 26 | row :status 27 | row :payment_method 28 | row :user 29 | row :created_at 30 | row :response_id 31 | row :full_response 32 | end 33 | active_admin_comments 34 | end 35 | 36 | action_item :refund, only: :show do 37 | link_to("Refund Payment", 38 | refunds_path(id: payment.id, type: Payment), 39 | method: "POST", 40 | class: "button", 41 | data: {confirm: "Are you sure you want to refund this payment?"}) 42 | end 43 | 44 | csv do 45 | column(:reference) 46 | column(:date) { |payment| payment.created_at.to_s(:sql) } 47 | column(:user_email) { |payment| payment.user.email } 48 | column(:price) 49 | column(:status) 50 | column(:payment_method) 51 | column(:response_id) 52 | column(:discount) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/views/affiliates/edit/_bank_account_form.html.slim: -------------------------------------------------------------------------------- 1 | .bank_account_form 2 | input(type="hidden" data-stripe="country" value="US") 3 | input(type="hidden" data-stripe="currency" value="USD") 4 | .row 5 | .form-group 6 | .col-sm-3 7 | = label_tag(:bank_routing_number, "Bank Routing Number", 8 | class: "control-label") 9 | .col-sm-5 10 | input.form-control.valid-field(data-stripe="routing_number" 11 | id="bank_routing_number") 12 | 13 | .row 14 | .form-group 15 | .col-sm-3 16 | = label_tag(:bank_account_number, "Account Number", 17 | class: "control-label") 18 | .col-sm-5 19 | input.form-control.valid-field(data-stripe="account_number" 20 | id="bank_account_number") 21 | 22 | .row 23 | .form-group 24 | .col-sm-3 25 | = label_tag(:account_holder_name, "Account Holder Name", 26 | class: "control-label") 27 | .col-sm-5 28 | input.form-control.valid-field(data-stripe="account_holder_name" 29 | id="account_holder_name") 30 | 31 | .row 32 | .form-group 33 | .col-sm-3 34 | = label_tag(:account_holder_type, 35 | "Account Holder Type (individual or company)", 36 | class: "control-label") 37 | .col-sm-5 38 | input.form-control.valid-field(data-stripe="account_holder_type" 39 | id="account_holder_type") 40 | -------------------------------------------------------------------------------- /spec/features/subscriptions/user_changes_a_subscription_plan_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "User changes a subscription plan", :vcr do 4 | let(:user) { create(:user) } 5 | let!(:plan) { create(:plan, remote_id: "orchestra_monthly", 6 | nickname: "Orchestra Monthly") } 7 | let!(:new_plan) { create(:plan, remote_id: "balcony_monthly", 8 | nickname: "Balcony Monthly") } 9 | let!(:subscription) { Subscription.create(user: user, plan: plan, 10 | start_date: Time.zone.now.to_date, end_date: plan.end_date_from, 11 | status: :waiting) } 12 | let(:token) { StripeToken.new( 13 | credit_card_number: "4242424242424242", expiration_month: "12", 14 | expiration_year: Time.zone.now.year + 1, cvc: "123") } 15 | let(:workflow) { CreatesSubscriptionViaStripe.new( 16 | user: user, expected_subscription_id: [subscription.id], token: token) } 17 | 18 | before(:example) do 19 | sign_in(user.email, user.password) 20 | workflow.run 21 | end 22 | 23 | scenario "via stripe" do 24 | visit user_path(user) 25 | 26 | within("#subscription_#{subscription.id}") do 27 | click_on("change_plan") 28 | end 29 | choose("new_plan_#{new_plan.id}") 30 | click_on("change_subscription_plan") 31 | 32 | expect(current_url).to match(user_path(user)) 33 | expect(page).to have_content("Subscription plan was successfully changed") 34 | end 35 | end -------------------------------------------------------------------------------- /db/migrate/20180307134551_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration[5.1] 2 | def self.up 3 | create_table :delayed_jobs, force: true do |table| 4 | table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. 6 | table.text :handler, null: false # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.string :queue # The name of the queue this job is in 13 | table.timestamps null: true 14 | end 15 | 16 | add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" 17 | end 18 | 19 | def self.down 20 | drop_table :delayed_jobs 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/ruby:2.5.0-node-browsers 6 | environment: 7 | - RAILS_ENV: test 8 | - PGHOST: 127.0.0.1 9 | - PGUSER: alex 10 | 11 | - image: circleci/postgres:10.1 12 | environment: 13 | POSTGRES_USER: alex 14 | POSTGRES_DB: snow_globe_test 15 | 16 | working_directory: ~/snow_globe_theater 17 | 18 | steps: 19 | - checkout 20 | 21 | - restore_cache: 22 | keys: 23 | - v1-dependencies-{{ checksum "Gemfile.lock" }} 24 | - v1-dependencies- 25 | 26 | - run: 27 | name: install dependencies 28 | command: | 29 | bundle install --jobs=4 --retry=3 --path vendor/bundle 30 | 31 | - save_cache: 32 | paths: 33 | - ./vendor/bundle 34 | key: v1-dependencies-{{ checksum "Gemfile.lock" }} 35 | 36 | - run: 37 | name: wait for db 38 | command: dockerize -wait tcp://localhost:5432 -timeout 1m 39 | 40 | - run: 41 | name: database setup 42 | command: | 43 | bundle exec rails db:schema:load --trace 44 | 45 | - run: 46 | name: run tests 47 | command: | 48 | bundle exec rspec --format progress 49 | 50 | - store_test_results: 51 | path: /tmp/test-results -------------------------------------------------------------------------------- /spec/workflows/adds_to_cart_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe AddsToCart do 4 | let(:user) { instance_double(User) } 5 | let(:performance) { instance_double(Performance) } 6 | let(:ticket_1) { instance_spy(Ticket, status: "unsold") } 7 | let(:ticket_2) { instance_spy(Ticket, status: "unsold") } 8 | 9 | context "when there are enough tickets to fulfill the order" do 10 | it "adds tickets to the shopping cart" do 11 | expect(performance).to receive(:unsold_tickets).with(1). 12 | and_return([ticket_1]) 13 | workflow = AddsToCart.new(user: user, performance: performance, count: 1) 14 | 15 | workflow.run 16 | 17 | expect(workflow.success).to be(true) 18 | expect(ticket_1).to have_received(:place_in_cart_for).with(user) 19 | expect(ticket_2).not_to have_received(:place_in_cart_for) 20 | end 21 | end 22 | 23 | context "when there are not enough tickets to fulfill the order" do 24 | it "does not add tickets to the shopping cart" do 25 | expect(performance).to receive(:unsold_tickets).with(1). 26 | and_return([]) 27 | workflow = AddsToCart.new(user: user, performance: performance, count: 1) 28 | 29 | workflow.run 30 | 31 | expect(workflow.success).to be(false) 32 | expect(ticket_1).not_to have_received(:place_in_cart_for) 33 | expect(ticket_2).not_to have_received(:place_in_cart_for) 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /app/workflows/prepares_stripe_refund.rb: -------------------------------------------------------------------------------- 1 | class PreparesStripeRefund 2 | 3 | attr_accessor :administrator, :refund_amount_cents, :payment_id, 4 | :success, :refund_payment, :refundable, :error 5 | 6 | delegate :save, to: :refund_payment 7 | 8 | def initialize(refundable:, administrator:, refund_amount_cents:) 9 | @refundable = refundable 10 | @administrator = administrator 11 | @refund_amount_cents = refund_amount_cents 12 | @success = false 13 | @error = nil 14 | end 15 | 16 | def pre_purchase_valid? 17 | refundable.present? 18 | end 19 | 20 | def run 21 | Payment.transaction do 22 | raise "not valid" unless pre_purchase_valid? 23 | self.refund_payment = generate_refund_payment.payment 24 | raise "can't refund that amount" unless 25 | refund_payment.can_refund?(refund_amount_cents) 26 | update_tickets 27 | RefundChargeJob.perform_later(refundable_id: refund_payment.id) 28 | self.success = true 29 | end 30 | rescue StandardError => exception 31 | self.error = exception.message 32 | on_failure 33 | end 34 | 35 | def on_failure 36 | self.success = false 37 | end 38 | 39 | def generate_refund_payment 40 | refundable.generate_refund_payment( 41 | amount_cents: refund_amount_cents, admin: administrator) 42 | end 43 | 44 | def update_tickets 45 | refundable.tickets.each(&:refund_pending!) 46 | end 47 | 48 | def success? 49 | success 50 | end 51 | end -------------------------------------------------------------------------------- /spec/features/shopping_cart/adding_to_cart_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "adding to cart" do 4 | let(:buyer) { User.create(email: "buyer@example.com", password: "password") } 5 | let(:play) { Event.create(name: "A Midsummer Night's Dream") } 6 | let(:first_performance) { play.performances.create( 7 | start_time: "2018-02-08 19:00:00") } 8 | let(:next_performance) { play.performances.create( 9 | start_time: "2018-02-09 19:00:00") } 10 | 11 | def create_tickets_for(performance) 12 | 2.times do 13 | Ticket.create(performance: performance, 14 | status: "unsold", 15 | price_cents: 1500) 16 | end 17 | end 18 | 19 | before do 20 | sign_in(buyer.email, buyer.password) 21 | create_tickets_for(first_performance) 22 | create_tickets_for(next_performance) 23 | end 24 | 25 | it "adds tickets to a cart" do 26 | visit event_path(play) 27 | within("#performance_#{first_performance.id}") do 28 | select("2", from: "ticket_count") 29 | click_on("add-to-cart") 30 | end 31 | 32 | expect(current_url).to match("cart") 33 | within("#event_#{play.id}") do 34 | within("#performance_#{first_performance.id}") do 35 | expect(page).to have_selector(".ticket_count", text: "2") 36 | expect(page).to have_selector(".subtotal", text: "$30") 37 | end 38 | expect(page).not_to have_selector("#performance_#{next_performance.id}") 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /spec/controllers/shopping_carts_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe ShoppingCartsController do 4 | describe "PATCH #update" do 5 | let(:user) { instance_spy(User) } 6 | let(:performance) { instance_spy( 7 | Performance, event: build_stubbed(:event)) } 8 | let(:workflow) { instance_spy(AddsToCart) } 9 | 10 | before(:example) do 11 | allow(controller).to receive(:current_user).and_return(user) 12 | allow(Performance).to receive(:find).with("2").and_return(performance) 13 | allow(AddsToCart).to receive(:new).with( 14 | user: user, performance: performance, count: "1").and_return(workflow) 15 | end 16 | 17 | context "when successful" do 18 | it "adds tickets to a shopping cart" do 19 | allow(workflow).to receive(:success).and_return(true) 20 | 21 | patch :update, params: { performance_id: "2", ticket_count: "1" } 22 | 23 | expect(workflow).to have_received(:run) 24 | expect(controller).to redirect_to(shopping_cart_path) 25 | end 26 | end 27 | 28 | context "when unsuccessful" do 29 | it "redirects back to the event" do 30 | allow(workflow).to receive(:success).and_return(false) 31 | 32 | patch :update, params: { performance_id: "2", ticket_count: "1" } 33 | 34 | expect(workflow).to have_received(:run) 35 | expect(controller).to redirect_to(performance.event) 36 | end 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /app/models/payment_line_item.rb: -------------------------------------------------------------------------------- 1 | class PaymentLineItem < ApplicationRecord 2 | 3 | has_paper_trail 4 | 5 | belongs_to :payment 6 | belongs_to :buyable, polymorphic: true 7 | 8 | has_many :refunds, class_name: "PaymentLineItem", 9 | foreign_key: "original_line_item_id" 10 | belongs_to :original_line_item, class_name: "PaymentLineItem", optional: true 11 | 12 | enum refund_status: {no_refund: 0, refund_pending: 1, refunded: 2} 13 | 14 | delegate :performance, to: :buyable, allow_nil: true 15 | delegate :event, to: :performance, allow_nil: true 16 | delegate :id, to: :event, prefix: true, allow_nil: true 17 | 18 | monetize :price_cents 19 | 20 | def self.tickets 21 | where(buyable_type: "Ticket") 22 | end 23 | 24 | def generate_refund_payment(amount_cents:, admin:, refund_payment: nil) 25 | refund_payment ||= Payment.create!( 26 | user_id: payment.user_id, price_cents: -amount_cents, 27 | status: "refund_pending", payment_method: payment.payment_method, 28 | original_payment_id: payment.id, administrator: admin, 29 | reference: Payment.generate_reference) 30 | PaymentLineItem.create!( 31 | buyable: buyable, price_cents: -price_cents, 32 | refund_status: "refund_pending", original_line_item_id: id, 33 | administrator_id: admin.id, payment: refund_payment) 34 | end 35 | 36 | def original_payment 37 | original_line_item&.payment 38 | end 39 | 40 | def tickets 41 | [buyable] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/workflows/creates_stripe_refund.rb: -------------------------------------------------------------------------------- 1 | class CreatesStripeRefund 2 | 3 | attr_accessor :payment_to_refund, :success, :stripe_refund 4 | 5 | def initialize(payment_to_refund:) 6 | @payment_to_refund = payment_to_refund 7 | @success = false 8 | end 9 | 10 | def run 11 | Payment.transaction do 12 | process_refund 13 | update_payment 14 | update_tickets 15 | on_success 16 | end 17 | rescue StandardError 18 | on_failure 19 | end 20 | 21 | def process_refund 22 | raise "No Such Payment" if payment_to_refund.nil? 23 | @stripe_refund = StripeRefund.new(payment_to_refund: payment_to_refund) 24 | @stripe_refund.refund 25 | raise "Refund failure" unless stripe_refund.success? 26 | end 27 | 28 | def update_payment 29 | payment_to_refund.update!(stripe_refund.refund_attributes) 30 | payment_to_refund.payment_line_items.each(&:refunded!) 31 | payment_to_refund.original_payment.refunded! if stripe_refund.success? 32 | end 33 | 34 | def update_tickets 35 | payment_to_refund.tickets.each(&:refund_successful) 36 | end 37 | 38 | def on_success 39 | RefundMailer.notify_success(payment_to_refund).deliver_later 40 | NotifyTaxCloudOfRefundJob.perform_later(payment_to_refund.original_payment) 41 | end 42 | 43 | def on_failure 44 | unrefund_tickets 45 | RefundMailer.notify_failure(payment_to_refund).deliver_later 46 | end 47 | 48 | def unrefund_tickets 49 | payment_to_refund.tickets.each(&:purchased!) 50 | end 51 | end -------------------------------------------------------------------------------- /spec/features/visitors/sign_up_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.feature "Sign Up", :devise do 4 | scenario "visitor can sign up with valid email and password" do 5 | sign_up_with("test@example.com", "password", "password") 6 | 7 | txts = [I18n.t("devise.registrations.signed_up"), 8 | I18n.t("devise.registrations.signed_up_but_unconfirmed")] 9 | expect(page).to have_content(/.*#{txts[0]}.*|.*#{txts[1]}.*/) 10 | end 11 | 12 | scenario "visitor cannot sign up with invalid email address" do 13 | sign_up_with("email", "password", "password") 14 | 15 | expect(page).to have_content("Email is invalid") 16 | end 17 | 18 | scenario "visitor cannot sign up without password" do 19 | sign_up_with("test@example.com", "", "") 20 | 21 | expect(page).to have_content("Password can't be blank") 22 | end 23 | 24 | scenario "visitor cannot sign up with a short password" do 25 | sign_up_with("test@example.com", "1234", "1234") 26 | 27 | expect(page).to have_content("Password is too short") 28 | end 29 | 30 | scenario "visitor cannot sign up without password confirmation" do 31 | sign_up_with("test@example.com", "password", "") 32 | 33 | expect(page).to have_content("Password confirmation doesn't match") 34 | end 35 | 36 | scenario "visitor cannot sign up with mismatched password and confirmation" do 37 | sign_up_with("test@example.com", "password", "mismatch") 38 | 39 | expect(page).to have_content("Password confirmation doesn't match") 40 | end 41 | end -------------------------------------------------------------------------------- /app/admin/users.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register User do 2 | permit_params :email, :password, :password_confirmation, :cellphone_number 3 | 4 | filter :email 5 | filter :current_sign_in_at 6 | filter :sign_in_count 7 | filter :created_at 8 | 9 | index do 10 | selectable_column 11 | id_column 12 | column :email 13 | column :current_sign_in_at 14 | column :sign_in_count 15 | column :created_at 16 | actions 17 | end 18 | 19 | form do |f| 20 | f.inputs "Admin Details" do 21 | f.input :email 22 | f.input :password 23 | f.input :password_confirmation 24 | f.input :cellphone_number 25 | end 26 | f.actions 27 | end 28 | 29 | action_item :simulation, only: :show do 30 | link_to( 31 | "Simulate User", user_simulation_path(id_to_simulate: resource.id), 32 | method: :post, class: "action-edit", 33 | data: {confirm: "Do you want to simulate this user?"}) 34 | end 35 | 36 | controller do 37 | def update 38 | @user = User.find(params[:id]) 39 | if params[:user][:password].blank? 40 | @user.update_without_password(permitted_params[:user]) 41 | else 42 | @user.update_attributes(permitted_params[:user]) 43 | end 44 | return if @user.admin? && params[:user][:cellphone_number].blank? 45 | authy = Authy::API.register_user( 46 | email: @user.email, 47 | cellphone: params[:user][:cellphone_number], 48 | country_code: "380") 49 | @user.update(authy_id: authy.id) if authy 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/workflows/executes_pay_pal_payment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe ExecutesPayPalPayment, :vcr, :aggregate_failures do 4 | describe "successful paypal payment" do 5 | let!(:ticket_1) { instance_spy( 6 | Ticket, status: "pending", price: Money.new(1500), id: 1) } 7 | let!(:ticket_2) { instance_spy( 8 | Ticket, status: "pending", price: Money.new(1500), id: 2) } 9 | let!(:ticket_3) { instance_spy(Ticket, status: "unsold", id: 3) } 10 | let(:payment) { instance_spy(Payment, tickets: [ticket_1, ticket_2]) } 11 | let(:pay_pal_payment) { instance_spy(PayPalPayment, execute: true) } 12 | let(:user) { instance_double( 13 | User, id: 5, tickets_in_cart: [ticket_1, ticket_2]) } 14 | let(:workflow) { ExecutesPayPalPayment.new( 15 | payment_id: "PAYMENT_ID", token: "TOKEN", payer_id: "PAYER_ID") } 16 | 17 | before(:example) do 18 | allow(workflow).to receive(:payment).and_return(payment) 19 | allow(workflow).to receive(:pay_pal_payment).and_return(pay_pal_payment) 20 | expect(NotifyTaxCloudJob).to receive(:perform_later).with(payment) 21 | workflow.run 22 | end 23 | 24 | it "updates the ticket status" do 25 | expect(ticket_1).to have_received(:purchased!) 26 | expect(ticket_2).to have_received(:purchased!) 27 | expect(ticket_3).not_to have_received(:purchased!) 28 | expect(payment).to have_received(:succeeded!) 29 | expect(pay_pal_payment).to have_received(:execute) 30 | expect(workflow.success).to be_truthy 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /app/models/stripe_charge.rb: -------------------------------------------------------------------------------- 1 | class StripeCharge 2 | 3 | attr_accessor :token, :payment, :response, :error 4 | 5 | def self.charge(token:, payment:) 6 | StripeCharge.new(token: token, payment: payment).charge 7 | end 8 | 9 | def initialize(token:, payment:) 10 | @token = token 11 | @payment = payment 12 | end 13 | 14 | def charge 15 | return if response.present? 16 | @response = Stripe::Charge.create( 17 | {amount: payment.price.cents, currency: "usd", 18 | source: token.id, description: "", 19 | metadata: {reference: payment.reference}}, 20 | idempotency_key: payment.reference) 21 | rescue Stripe::StripeError => e 22 | @response = nil 23 | @error = e 24 | end 25 | 26 | def charge_parameters 27 | parameters = { 28 | amount: payment.price.cents, currency: "usd", 29 | source: token.id, description: "", 30 | metadata: {reference: payment.reference}} 31 | if payment.active_affiliate.present? 32 | parameters[:destination] = payment.affiliate.stripe_id 33 | parameters[:application_fee] = payment.application_fee.cents 34 | end 35 | parameters 36 | end 37 | 38 | def success? 39 | response || !error 40 | end 41 | 42 | def payment_attributes 43 | success? ? success_attributes : failure_attributes 44 | end 45 | 46 | def success_attributes 47 | {status: :succeeded, 48 | response_id: response.id, full_response: response.to_json} 49 | end 50 | 51 | def failure_attributes 52 | {status: :failed, full_response: error.to_json} 53 | end 54 | end -------------------------------------------------------------------------------- /db/migrate/20180207163454_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeviseCreateUsers < ActiveRecord::Migration[5.1] 4 | def change 5 | create_table :users do |t| 6 | ## Database authenticatable 7 | t.string :email, null: false, default: "" 8 | t.string :encrypted_password, null: false, default: "" 9 | 10 | ## Recoverable 11 | t.string :reset_password_token 12 | t.datetime :reset_password_sent_at 13 | 14 | ## Rememberable 15 | t.datetime :remember_created_at 16 | 17 | ## Trackable 18 | t.integer :sign_in_count, default: 0, null: false 19 | t.datetime :current_sign_in_at 20 | t.datetime :last_sign_in_at 21 | t.inet :current_sign_in_ip 22 | t.inet :last_sign_in_ip 23 | 24 | ## Confirmable 25 | # t.string :confirmation_token 26 | # t.datetime :confirmed_at 27 | # t.datetime :confirmation_sent_at 28 | # t.string :unconfirmed_email # Only if using reconfirmable 29 | 30 | ## Lockable 31 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 32 | # t.string :unlock_token # Only if unlock strategy is :email or :both 33 | # t.datetime :locked_at 34 | 35 | 36 | t.timestamps null: false 37 | end 38 | 39 | add_index :users, :email, unique: true 40 | add_index :users, :reset_password_token, unique: true 41 | # add_index :users, :confirmation_token, unique: true 42 | # add_index :users, :unlock_token, unique: true 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/views/affiliates/new.html.slim: -------------------------------------------------------------------------------- 1 | h1 Make Me an Affiliate 2 | 3 | = form_for(Affiliate.new) do |f| 4 | .row 5 | .form-group 6 | .col-sm-1 7 | = check_box_tag(:tos, "1", class: "form_control") 8 | .col-sm-5 9 | = label_tag(:tos, class: "control-label") do 10 | | By registering your account, you agree to our Services Agreement 11 | and the 12 | = link_to " Stripe Connected Account Agreement ", 13 | "https://stripe.com/us/connect-account/legal" 14 | .row 15 | .form-group 16 | = f.submit("Make Me an Affiliate", class: "btn btn-default") 17 | 18 | h2 Terms 19 | 20 | | Payment processing services for affiliates on the Snow Globe Theater site 21 | are provided by Stripe and are subject to the 22 | = link_to " Stripe Connected Account Agreement ", 23 | "https://stripe.com/us/connect-account/legal" 24 | | which includes the 25 | = link_to " Stripe Terms of Service ", "https://stripe.com/us/legal" 26 | | (collectively, the "Stripe Services Agreement"). By agreeing to 27 | this agreement or continuing to operate as an affiliate for the 28 | Snow Globe Theater, you agree to be bound by the Stripe Services 29 | Agreement, as the same may be modified by Stripe from time to time. 30 | As a condition of the Snow Globe Theater enabling payment processing 31 | services through Stripe, you agree to provide the Snow Globe Theater 32 | accurate and complete information about you and your business, and you 33 | authorize the Snow Globe Theater to share it and transaction information 34 | related to your use of the payment processing services provided by Stripe. -------------------------------------------------------------------------------- /lib/tasks/consistency_test.rake: -------------------------------------------------------------------------------- 1 | namespace :snow_globe do 2 | 3 | task check_consistency: :environment do 4 | inconsistent = Payment.all.reject do |payment| 5 | TicketPaymentConsistency.new(payment).consistent? 6 | end 7 | if inconsistent.empty? 8 | ConsistencyMailer.all_is_well.deliver 9 | else 10 | ConsistencyMailer.inconsistencies_detected(inconsistent).deliver 11 | end 12 | end 13 | end 14 | 15 | class TicketPaymentConsistency < SimpleDelegator 16 | 17 | attr_accessor :errors 18 | 19 | def initialize(payment) 20 | super 21 | @errors = [] 22 | end 23 | 24 | def consistent? 25 | success_consistent 26 | refund_consistent 27 | amount_consistent 28 | errors.empty? 29 | end 30 | 31 | def success_consistent 32 | return unless succeeded? 33 | inconsistent_tickets = tickets.select { |ticket| !ticket.purchased? } 34 | inconsistent_tickets.each do |ticket| 35 | @errors << "Successful purchase #{id}, ticket #{ticket.id} not purchased" 36 | end 37 | end 38 | 39 | def refund_consistent 40 | return unless refund? 41 | inconsistent_tickets = tickets.select { |ticket| !ticket.refunded? } 42 | inconsistent_tickets.each do |ticket| 43 | @errors << "Refunded purchase #{id}, ticket #{ticket.id} not refunded" 44 | end 45 | end 46 | 47 | def amount_consistent 48 | return if payment_line_items.map(&:price).first.nil? 49 | expected = payment_line_items.map(&:price).first - discount 50 | return if expected == price 51 | @errors << 52 | "Purchase #{id}, expected price #{expected}, actual price #{price}" 53 | end 54 | end -------------------------------------------------------------------------------- /db/migrate/20180409081143_create_versions.rb: -------------------------------------------------------------------------------- 1 | # This migration creates the `versions` table, the only schema PT requires. 2 | # All other migrations PT provides are optional. 3 | class CreateVersions < ActiveRecord::Migration[5.1] 4 | 5 | # The largest text column available in all supported RDBMS is 6 | # 1024^3 - 1 bytes, roughly one gibibyte. We specify a size 7 | # so that MySQL will use `longtext` instead of `text`. Otherwise, 8 | # when serializing very large objects, `text` might not be big enough. 9 | TEXT_BYTES = 1_073_741_823 10 | 11 | def change 12 | create_table :versions do |t| 13 | t.string :item_type, {:null=>false} 14 | t.integer :item_id, null: false 15 | t.string :event, null: false 16 | t.string :whodunnit 17 | t.text :object, limit: TEXT_BYTES 18 | 19 | # Known issue in MySQL: fractional second precision 20 | # ------------------------------------------------- 21 | # 22 | # MySQL timestamp columns do not support fractional seconds unless 23 | # defined with "fractional seconds precision". MySQL users should manually 24 | # add fractional seconds precision to this migration, specifically, to 25 | # the `created_at` column. 26 | # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html) 27 | # 28 | # MySQL users should also upgrade to rails 4.2, which is the first 29 | # version of ActiveRecord with support for fractional seconds in MySQL. 30 | # (https://github.com/rails/rails/pull/14359) 31 | # 32 | t.datetime :created_at 33 | end 34 | add_index :versions, %i(item_type item_id) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/workflows/adds_affiliate_to_cart_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe AddsAffiliateToCart do 4 | let(:user) { create(:user) } 5 | let!(:affiliate) { create(:affiliate, tag: "tag") } 6 | 7 | it "adds tag to cart if cart exists" do 8 | workflow = AddsAffiliateToCart.new(tag: "tag", user: user) 9 | 10 | workflow.run 11 | 12 | expect(ShoppingCart.for(user:user).affiliate).to eq(affiliate) 13 | end 14 | 15 | it "manages if the tag doesn't exist" do 16 | workflow = AddsAffiliateToCart.new(tag: "banana", user: user) 17 | 18 | workflow.run 19 | 20 | expect(ShoppingCart.for(user: user).affiliate).to be_nil 21 | end 22 | 23 | it "manages if the tag is nil" do 24 | workflow = AddsAffiliateToCart.new(tag: nil, user: user) 25 | 26 | workflow.run 27 | 28 | expect(ShoppingCart.for(user: user).affiliate).to be_nil 29 | end 30 | 31 | it "correctly adds tag if the case is wrong" do 32 | workflow = AddsAffiliateToCart.new(tag: "TAG", user: user) 33 | 34 | workflow.run 35 | 36 | expect(ShoppingCart.for(user: user).affiliate).to eq(affiliate) 37 | end 38 | 39 | it "does nothing if there is no current user" do 40 | workflow = AddsAffiliateToCart.new(tag: "tag", user: nil) 41 | 42 | workflow.run 43 | 44 | expect(ShoppingCart.for(user: nil)).to be_nil 45 | end 46 | 47 | it "does nothing if the affiliate belongs to the user" do 48 | affiliate.update(user: user) 49 | workflow = AddsAffiliateToCart.new(tag: "tag", user: user) 50 | 51 | workflow.run 52 | 53 | expect(ShoppingCart.for(user: user).affiliate).to be_nil 54 | end 55 | end -------------------------------------------------------------------------------- /app/models/shopping_cart.rb: -------------------------------------------------------------------------------- 1 | class ShoppingCart < ApplicationRecord 2 | 3 | belongs_to :user 4 | belongs_to :address, optional: true 5 | belongs_to :discount_code, optional: true 6 | belongs_to :affiliate, optional: true 7 | 8 | enum shipping_method: {electronic: 0, standard: 1, overnight: 2} 9 | 10 | def self.for(user:) 11 | return nil unless user 12 | ShoppingCart.find_or_create_by(user_id: user.id) 13 | end 14 | 15 | def price_calculator 16 | @price_calculator ||= PriceCalculator.new( 17 | tickets, discount_code, shipping_method.to_s, 18 | user: user, address: address, tax_id: "cart_#{id}") 19 | end 20 | 21 | delegate :processing_fee, :shipping_fee, :sales_tax, to: :price_calculator 22 | 23 | def total_cost 24 | price_calculator.total_price 25 | end 26 | 27 | def tickets 28 | @tickets ||= user.tickets_in_cart 29 | end 30 | 31 | def events 32 | tickets.map(&:event).uniq.sort_by(&:name) 33 | end 34 | 35 | def tickets_by_performance 36 | tickets.group_by { |t| t.performance.id } 37 | end 38 | 39 | def performance_count 40 | tickets_by_performance.each_pair.each_with_object({}) do |pair, result| 41 | result[pair.first] = pair.last.size 42 | end 43 | end 44 | 45 | def performances_for(event) 46 | tickets.map(&:performance) 47 | .select { |performance| performance.event == event } 48 | .uniq.sort_by(&:start_time) 49 | end 50 | 51 | def subtotal_for(performance) 52 | tickets_by_performance[performance.id].sum(&:price) 53 | end 54 | 55 | def item_attribute 56 | :ticket_ids 57 | end 58 | 59 | def item_ids 60 | tickets.map(&:id) 61 | end 62 | end -------------------------------------------------------------------------------- /spec/jobs/build_day_revenue_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe BuildDayRevenueJob, type: :job do 4 | let!(:really_old_payment) { create( 5 | :payment, created_at: 1.month.ago, price_cents: 4500) } 6 | let!(:really_old_payment_2) { create( 7 | :payment, created_at: 1.month.ago, price_cents: 1500) } 8 | let!(:old_payment) { create( 9 | :payment, created_at: 2.days.ago, price_cents: 3500) } 10 | let!(:yesterday_payment) { create( 11 | :payment, created_at: 1.day.ago, price_cents: 2500) } 12 | let!(:now_payment) { create( 13 | :payment, created_at: 1.second.ago, price_cents: 1500) } 14 | 15 | it "runs the report" do 16 | BuildDayRevenueJob.perform_now 17 | 18 | expect(DayRevenue.find_by(day: 1.month.ago).price_cents).to eq(6000) 19 | expect(DayRevenue.find_by(day: 2.days.ago).price_cents).to eq(3500) 20 | expect(DayRevenue.find_by(day: 1.day.ago)).to be_nil 21 | expect(DayRevenue.find_by(day: Date.current)).to be_nil 22 | end 23 | 24 | it "runs the report twice" do 25 | BuildDayRevenueJob.perform_now 26 | BuildDayRevenueJob.perform_now 27 | 28 | expect(DayRevenue.count).to eq(2) 29 | expect(DayRevenue.find_by(day: 1.month.ago).price_cents).to eq(6000) 30 | end 31 | 32 | context "it calculates tickets" do 33 | let!(:performance) { create(:performance, event: create(:event)) } 34 | let!(:ticket) { create(:ticket, performance: performance) } 35 | let!(:payment_line_item) { create( 36 | :payment_line_item, payment: really_old_payment, buyable: ticket, 37 | created_at: 1.month.ago) } 38 | 39 | it "adds ticket count" do 40 | BuildDayRevenueJob.perform_now 41 | 42 | expect(DayRevenue.find_by(day: 1.month.ago).ticket_count).to eq(1) 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /spec/services/tax_calculator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe TaxCalculator, :aggregate_failures, :vcr do 4 | let(:address) { build_stubbed(:address) } 5 | let(:user) { build_stubbed(:user) } 6 | let(:calculator) { TaxCalculator.new( 7 | user: user, address: address, cart_id: "1", 8 | items: [ 9 | TaxCalculator::Item.create(:ticket, 1, Money.new(3000)), 10 | TaxCalculator::Item.create(:processing, 1, Money.new(100)), 11 | TaxCalculator::Item.create(:shipping, 1, Money.new(200)) 12 | ]) } 13 | let(:transaction) { calculator.transaction } 14 | 15 | describe "creation" do 16 | it "creates a transaction properly" do 17 | expect(transaction).to have_attributes(customer_id: user.id, cart_id: "1") 18 | expect(transaction.origin).to have_attributes( 19 | address1: "1060 W. Addison", address2: nil, 20 | city: "Chicago", state: "IL", zip5: "60613") 21 | expect(transaction.destination).to have_attributes( 22 | address1: address.address_1, address2: address.address_2, 23 | city: address.city, state: address.state, zip5: address.zip) 24 | expect(transaction.cart_items.first).to have_attributes( 25 | index: 0, item_id: "Ticket", 26 | tic: "91083", price: 30.00, quantity: 1) 27 | expect(transaction.cart_items.second).to have_attributes( 28 | index: 1, item_id: "Processing", 29 | tic: "10010", price: 1.00, quantity: 1) 30 | expect(transaction.cart_items.third).to have_attributes( 31 | index: 2, item_id: "Shipping", 32 | tic: "11000", price: 2.00, quantity: 1) 33 | end 34 | 35 | it "handles a lookup correctly" do 36 | expect(calculator.tax_amount).to eq(0.30.to_money) 37 | expect(calculator.itemized_taxes).to eq( 38 | ticket_cents: 0.0, processing_cents: 10.0, shipping_cents: 20.0) 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /app/models/stripe_account.rb: -------------------------------------------------------------------------------- 1 | class StripeAccount 2 | 3 | attr_accessor :affiliate, :account, :tos_checked, :request_ip 4 | 5 | def initialize(affiliate, tos_checked: false, request_ip: false, account: nil) 6 | @affiliate = affiliate 7 | @tos_checked = tos_checked 8 | @request_ip = request_ip 9 | @account = account 10 | end 11 | 12 | def account 13 | @account ||= begin 14 | if affiliate.stripe_id.blank? 15 | create_account 16 | else 17 | retreive_account 18 | end 19 | end 20 | end 21 | 22 | def update_affiliate_verification 23 | Affiliate.transaction do 24 | affiliate.update( 25 | stripe_charges_enabled: account.charges_enabled, 26 | stripe_transfers_enabled: account.payouts_enabled, 27 | stripe_disabled_reason: account.verification.disabled_reason, 28 | stripe_validation_due_by: account.verification.due_by, 29 | verification_needed: account.verification.fields_needed) 30 | end 31 | end 32 | 33 | def update(values) 34 | update_from_hash(account, values) 35 | self.account = account.save 36 | update_affiliate_verification 37 | end 38 | 39 | private 40 | 41 | def update_from_hash(object, values) 42 | values.each do |key, value| 43 | if value.is_a?(Hash) 44 | sub_object = object.send(key.to_sym) 45 | update_from_hash(sub_object, value) 46 | elsif value.present? 47 | object.send(:"#{key}=", value) 48 | end 49 | end 50 | end 51 | 52 | def create_account 53 | account_params = {country: affiliate.country, type: "custom"} 54 | if tos_checked 55 | account_params[:tos_acceptance] = {date: Time.now.to_i, ip: request_ip} 56 | end 57 | Stripe::Account.create(account_params) 58 | end 59 | 60 | def retreive_account 61 | Stripe::Account.retrieve(affiliate.stripe_id) 62 | end 63 | end -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |If you are the application owner check the logs for more information.
64 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |