diff --git a/.browserslistrc b/.browserslistrc
new file mode 100644
index 000000000..e94f8140c
--- /dev/null
+++ b/.browserslistrc
@@ -0,0 +1 @@
+defaults
diff --git a/.gitignore b/.gitignore
index 54cb8bbbc..c2a266fe5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,37 @@ capybara-*.html
/spec/tmp/*
**.orig
rerun.txt
-pickle-email-*.html
\ No newline at end of file
+pickle-email-*.html
+# See https://help.github.com/articles/ignoring-files for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile '~/.gitignore_global'
+
+# Ignore bundler config.
+/.bundle
+
+# Ignore all logfiles and tempfiles.
+/log/*
+/tmp/*
+!/log/.keep
+!/tmp/.keep
+
+# Ignore pidfiles, but keep the directory.
+/tmp/pids/*
+!/tmp/pids/
+!/tmp/pids/.keep
+
+
+/public/assets
+.byebug_history
+
+# Ignore master key for decrypting credentials and more.
+/config/master.key
+
+/public/packs
+/public/packs-test
+/node_modules
+/yarn-error.log
+yarn-debug.log*
+.yarn-integrity
diff --git a/.rspec b/.rspec
new file mode 100644
index 000000000..c99d2e739
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--require spec_helper
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 000000000..ccfb6efd9
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+ruby-2.7.0
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 000000000..fbb4a7c45
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,28 @@
+source 'https://rubygems.org'
+git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ruby '2.7.0'
+
+gem 'rails', '~> 6.0.3', '>= 6.0.3.6'
+gem 'pg', '>= 0.18', '< 2.0'
+gem 'puma', '~> 4.1'
+gem 'webpacker', '~> 4.0'
+gem 'jbuilder', '~> 2.7'
+gem 'bootsnap', '>= 1.4.2', require: false
+gem 'draper'
+
+group :development do
+ gem 'listen', '~> 3.2'
+end
+
+group :development, :test do
+ gem 'byebug'
+end
+
+group :test do
+ gem 'database_cleaner-active_record'
+ gem 'rspec-rails', '~> 5.0.0'
+ gem 'capybara'
+ gem "factory_bot_rails"
+ gem 'faker'
+end
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 000000000..e85f91391
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,6 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require_relative 'config/application'
+
+Rails.application.load_tasks
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
new file mode 100644
index 000000000..09705d12a
--- /dev/null
+++ b/app/controllers/application_controller.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/app/controllers/charges_controller.rb b/app/controllers/charges_controller.rb
new file mode 100644
index 000000000..c93b3a1dd
--- /dev/null
+++ b/app/controllers/charges_controller.rb
@@ -0,0 +1,19 @@
+class ChargesController < ApplicationController
+ def index
+ render :index, locals: { view: ChargesViewObject.new(view_object_context) }
+ end
+
+ private
+
+ def view_object_context
+ {
+ successful_charges: ChargeDecorator.decorate_collection(charges_repo.all_by_status(:successful)),
+ failed_charges: ChargeDecorator.decorate_collection(charges_repo.all_by_status(:failed)),
+ disputed_charges: ChargeDecorator.decorate_collection(charges_repo.all_by_status(:disputed))
+ }
+ end
+
+ def charges_repo
+ @charges_repo ||= ChargeRepo.new(model: Charge, customer_model: Customer)
+ end
+end
\ No newline at end of file
diff --git a/app/decorators/application_decorator.rb b/app/decorators/application_decorator.rb
new file mode 100644
index 000000000..bc2d5681d
--- /dev/null
+++ b/app/decorators/application_decorator.rb
@@ -0,0 +1,2 @@
+class ApplicationDecorator < Draper::Decorator
+end
diff --git a/app/decorators/charge_decorator.rb b/app/decorators/charge_decorator.rb
new file mode 100644
index 000000000..8d7aec4e6
--- /dev/null
+++ b/app/decorators/charge_decorator.rb
@@ -0,0 +1,25 @@
+class ChargeDecorator < ApplicationDecorator
+ def customer_name
+ helpers.content_tag :p do
+ "Customer name: #{customer.first_name} #{customer.last_name}"
+ end
+ end
+
+ def amount
+ helpers.content_tag :p do
+ "Amount: #{object.amount}"
+ end
+ end
+
+ def created
+ helpers.content_tag :p do
+ "Charge date: #{object.created.strftime('%a %m/%d/%y%l:%M %p')}"
+ end
+ end
+
+ private
+
+ def customer
+ @customer ||= object.customer
+ end
+end
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
new file mode 100644
index 000000000..2d658d894
--- /dev/null
+++ b/app/javascript/packs/application.js
@@ -0,0 +1,16 @@
+// This file is automatically compiled by Webpack, along with any other files
+// present in this directory. You're encouraged to place your actual application logic in
+// a relevant structure within app/javascript and only use these pack files to reference
+// that code so it'll be compiled.
+import '../stylesheets/charges.css'
+
+require("@rails/ujs").start()
+
+
+
+// Uncomment to copy all static images under ../images to the output folder and reference
+// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
+// or the `imagePath` JavaScript helper below.
+//
+// const images = require.context('../images', true)
+// const imagePath = (name) => images(name, true)
diff --git a/app/javascript/stylesheets/charges.css b/app/javascript/stylesheets/charges.css
new file mode 100644
index 000000000..3eea8f45e
--- /dev/null
+++ b/app/javascript/stylesheets/charges.css
@@ -0,0 +1,18 @@
+.charges {
+ display: flex;
+}
+.charges_block {
+ flex: 1;
+}
+
+.charges_block__list_failed {
+ background-color: #FF0000
+}
+
+.charges_block__list_disputed {
+ background-color: #FF5400
+}
+
+.charges_block__title {
+ text-align: center;
+}
\ No newline at end of file
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
new file mode 100644
index 000000000..10a4cba84
--- /dev/null
+++ b/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/app/models/charge.rb b/app/models/charge.rb
new file mode 100644
index 000000000..dfd0776c5
--- /dev/null
+++ b/app/models/charge.rb
@@ -0,0 +1,5 @@
+class Charge < ApplicationRecord
+ USD_CURRENCY = 'USD'.freeze
+
+ belongs_to :customer
+end
\ No newline at end of file
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/models/customer.rb b/app/models/customer.rb
new file mode 100644
index 000000000..4a5d9c685
--- /dev/null
+++ b/app/models/customer.rb
@@ -0,0 +1,3 @@
+class Customer < ApplicationRecord
+ has_many :charges, dependent: :restrict_with_exception
+end
\ No newline at end of file
diff --git a/app/repos/application_repo.rb b/app/repos/application_repo.rb
new file mode 100644
index 000000000..af1f034d5
--- /dev/null
+++ b/app/repos/application_repo.rb
@@ -0,0 +1,7 @@
+class ApplicationRepo
+ attr_reader :model
+
+ def initialize(model:)
+ @model = model
+ end
+end
\ No newline at end of file
diff --git a/app/repos/charge_repo.rb b/app/repos/charge_repo.rb
new file mode 100644
index 000000000..fe7026a19
--- /dev/null
+++ b/app/repos/charge_repo.rb
@@ -0,0 +1,41 @@
+class ChargeRepo < ApplicationRepo
+ CHARGE_STATUSES = {
+ successful: { paid: true, refunded: false },
+ failed: { paid: false, refunded: true },
+ disputed: { paid: false, refunded: false }
+ }.freeze
+
+ attr_reader :customer_model
+
+ def initialize(model:, customer_model: nil)
+ @customer_model = customer_model
+ super(model: model)
+ end
+
+ def create!(customer_id:, amount:, created:, status:)
+ record = build_default(customer_id: customer_id, amount: amount, created: created)
+ record.assign_attributes(CHARGE_STATUSES[status])
+ record.save!
+ end
+
+ def all_by_status(status)
+ model.includes(customer_name).where(CHARGE_STATUSES[status]).references(customer_name).all
+ end
+
+ private
+
+ def customer_name
+ @_customer_name ||= customer_model.name.downcase
+ end
+
+ def build_default(customer_id:, amount:, created:)
+ model.new(
+ paid: true,
+ currency: Charge::USD_CURRENCY,
+ refunded: false,
+ customer_id: customer_id,
+ amount: amount,
+ created: created
+ )
+ end
+end
\ No newline at end of file
diff --git a/app/view_objects/application_view_object.rb b/app/view_objects/application_view_object.rb
new file mode 100644
index 000000000..0d32c2a26
--- /dev/null
+++ b/app/view_objects/application_view_object.rb
@@ -0,0 +1,7 @@
+class ApplicationViewObject < BasicObject
+ attr_reader :context
+
+ def initialize(context)
+ @context = context
+ end
+end
\ No newline at end of file
diff --git a/app/view_objects/charges_view_object.rb b/app/view_objects/charges_view_object.rb
new file mode 100644
index 000000000..f0a1ee55b
--- /dev/null
+++ b/app/view_objects/charges_view_object.rb
@@ -0,0 +1,28 @@
+class ChargesViewObject < ApplicationViewObject
+ def successful_charges
+ {
+ charges: context[:successful_charges],
+ title: 'Successful Charges',
+ id: 'successful-charges',
+ list_class: ''
+ }
+ end
+
+ def failed_charges
+ {
+ charges: context[:failed_charges],
+ title: 'Failed Charges',
+ id: 'failed-charges',
+ list_class: 'charges_block__list_failed'
+ }
+ end
+
+ def disputed_charges
+ {
+ charges: context[:disputed_charges],
+ title: 'Disputed Charges',
+ id: 'disputed-charges',
+ list_class: 'charges_block__list_disputed'
+ }
+ end
+end
\ No newline at end of file
diff --git a/app/views/charges/_charges.html.erb b/app/views/charges/_charges.html.erb
new file mode 100644
index 000000000..03451410f
--- /dev/null
+++ b/app/views/charges/_charges.html.erb
@@ -0,0 +1,13 @@
+
>
+
<%= title %>
+
+
+ <% charges.each do |charge| %>
+ - >
+ <%= charge.customer_name %>
+ <%= charge.amount %>
+ <%= charge.created %>
+
+ <% end %>
+
+
\ No newline at end of file
diff --git a/app/views/charges/index.html.erb b/app/views/charges/index.html.erb
new file mode 100644
index 000000000..e61c0d5ad
--- /dev/null
+++ b/app/views/charges/index.html.erb
@@ -0,0 +1,5 @@
+
+ <%= render partial: 'charges', locals: view.failed_charges %>
+ <%= render partial: 'charges', locals: view.disputed_charges %>
+ <%= render partial: 'charges', locals: view.successful_charges %>
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
new file mode 100644
index 000000000..b296aba8e
--- /dev/null
+++ b/app/views/layouts/application.html.erb
@@ -0,0 +1,14 @@
+
+
+
+ Source
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+
+ <%= javascript_pack_tag 'application' %>
+
+
+
+ <%= yield %>
+
+
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 000000000..12f98da5a
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,72 @@
+module.exports = function(api) {
+ var validEnv = ['development', 'test', 'production']
+ var currentEnv = api.env()
+ var isDevelopmentEnv = api.env('development')
+ var isProductionEnv = api.env('production')
+ var isTestEnv = api.env('test')
+
+ if (!validEnv.includes(currentEnv)) {
+ throw new Error(
+ 'Please specify a valid `NODE_ENV` or ' +
+ '`BABEL_ENV` environment variables. Valid values are "development", ' +
+ '"test", and "production". Instead, received: ' +
+ JSON.stringify(currentEnv) +
+ '.'
+ )
+ }
+
+ return {
+ presets: [
+ isTestEnv && [
+ '@babel/preset-env',
+ {
+ targets: {
+ node: 'current'
+ }
+ }
+ ],
+ (isProductionEnv || isDevelopmentEnv) && [
+ '@babel/preset-env',
+ {
+ forceAllTransforms: true,
+ useBuiltIns: 'entry',
+ corejs: 3,
+ modules: false,
+ exclude: ['transform-typeof-symbol']
+ }
+ ]
+ ].filter(Boolean),
+ plugins: [
+ 'babel-plugin-macros',
+ '@babel/plugin-syntax-dynamic-import',
+ isTestEnv && 'babel-plugin-dynamic-import-node',
+ '@babel/plugin-transform-destructuring',
+ [
+ '@babel/plugin-proposal-class-properties',
+ {
+ loose: true
+ }
+ ],
+ [
+ '@babel/plugin-proposal-object-rest-spread',
+ {
+ useBuiltIns: true
+ }
+ ],
+ [
+ '@babel/plugin-transform-runtime',
+ {
+ helpers: false,
+ regenerator: true,
+ corejs: false
+ }
+ ],
+ [
+ '@babel/plugin-transform-regenerator',
+ {
+ async: false
+ }
+ ]
+ ].filter(Boolean)
+ }
+}
diff --git a/bin/rails b/bin/rails
new file mode 100755
index 000000000..073966023
--- /dev/null
+++ b/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 000000000..17240489f
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 000000000..5853b5ea8
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,36 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+FileUtils.chdir APP_ROOT do
+ # This script is a way to setup or update your development environment automatically.
+ # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
+ # Add necessary setup steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ # Install JavaScript dependencies
+ # system('bin/yarn')
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?('config/database.yml')
+ # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
+ # end
+
+ puts "\n== Preparing database =="
+ system! 'bin/rails db:prepare'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/bin/webpack b/bin/webpack
new file mode 100755
index 000000000..1031168d0
--- /dev/null
+++ b/bin/webpack
@@ -0,0 +1,18 @@
+#!/usr/bin/env ruby
+
+ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
+ENV["NODE_ENV"] ||= "development"
+
+require "pathname"
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require "bundler/setup"
+
+require "webpacker"
+require "webpacker/webpack_runner"
+
+APP_ROOT = File.expand_path("..", __dir__)
+Dir.chdir(APP_ROOT) do
+ Webpacker::WebpackRunner.run(ARGV)
+end
diff --git a/bin/yarn b/bin/yarn
new file mode 100755
index 000000000..460dd565b
--- /dev/null
+++ b/bin/yarn
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+APP_ROOT = File.expand_path('..', __dir__)
+Dir.chdir(APP_ROOT) do
+ begin
+ exec "yarnpkg", *ARGV
+ rescue Errno::ENOENT
+ $stderr.puts "Yarn executable was not detected in the system."
+ $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
+ exit 1
+ end
+end
diff --git a/config.ru b/config.ru
new file mode 100644
index 000000000..f7ba0b527
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,5 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+run Rails.application
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 000000000..700c95cba
--- /dev/null
+++ b/config/application.rb
@@ -0,0 +1,16 @@
+require_relative 'boot'
+
+require "rails"
+require "active_model/railtie"
+require "active_record/railtie"
+require "action_controller/railtie"
+require "action_view/railtie"
+
+Bundler.require(*Rails.groups)
+
+module Source
+ class Application < Rails::Application
+ config.load_defaults 6.0
+ config.generators.system_tests = nil
+ end
+end
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 000000000..b9e460cef
--- /dev/null
+++ b/config/boot.rb
@@ -0,0 +1,4 @@
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
+require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc
new file mode 100644
index 000000000..27ad900d5
--- /dev/null
+++ b/config/credentials.yml.enc
@@ -0,0 +1 @@
+JQ6TjopiUB+bfNUdcxKKCQtjJz1AyWHeDhtGVDY5GdPFXG+xgubod9UIdk+0REHY2ZAzXuggBcskTOWCD8gONQDBCcwKHDZyJdkJj39auIvaAs38Umrb89MwPRDOP5ciBib7VUMpEMkJ/IVyNFqdElHOHb7Y6woXp+yPfFRtusU42YnKkk43+lJagSMkdwOHDdeW/PfdQgrro+Ctxntej71DM8ll4MwlmmBe5ht/HKpE3Wn1TldM1UaswW6JkAQlL4e+rp12hSmHJi9Wi5CaJh4Cncb3gfqEmZkqLUKllh8z+t32hArA3NTCeW0IH0ByLB8VFXhXt2WBNKW2xD1Ji2/o14Lbo/QgUTQD0wFdc/OJseIcBBEZBHBa/++gSOzW79oaHU7CE4la7b7pJuz+Gm8xOes3iJ9IzUOw--gifJV/r50LPYrmCD--iyr5d4XPklQFHda5PoMKnA==
\ No newline at end of file
diff --git a/config/database.yml b/config/database.yml
new file mode 100644
index 000000000..3cda1a3ba
--- /dev/null
+++ b/config/database.yml
@@ -0,0 +1,14 @@
+default: &default
+ adapter: postgresql
+ encoding: unicode
+ # For details on connection pooling, see Rails configuration guide
+ # https://guides.rubyonrails.org/configuring.html#database-pooling
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+
+development:
+ <<: *default
+ database: source_development
+
+test:
+ <<: *default
+ database: source_test
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 000000000..426333bb4
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 000000000..75aadbef4
--- /dev/null
+++ b/config/environments/development.rb
@@ -0,0 +1,24 @@
+Rails.application.configure do
+ config.cache_classes = false
+ config.eager_load = false
+ config.consider_all_requests_local = true
+
+ if Rails.root.join('tmp', 'caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+ config.action_controller.enable_fragment_cache_logging = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{2.days.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ config.active_support.deprecation = :log
+ config.active_record.migration_error = :page_load
+ config.active_record.verbose_query_logs = true
+ config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+end
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 000000000..30825504b
--- /dev/null
+++ b/config/environments/test.rb
@@ -0,0 +1,14 @@
+Rails.application.configure do
+ config.cache_classes = true
+ config.eager_load = false
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
+ }
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+ config.cache_store = :null_store
+ config.action_dispatch.show_exceptions = false
+ config.action_controller.allow_forgery_protection = false
+ config.active_support.deprecation = :stderr
+end
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
new file mode 100644
index 000000000..c0d0a0251
--- /dev/null
+++ b/config/initializers/cookies_serializer.rb
@@ -0,0 +1 @@
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 000000000..c4bf1087b
--- /dev/null
+++ b/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1 @@
+Rails.application.config.filter_parameters += [:password]
diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb
new file mode 100644
index 000000000..5e3405431
--- /dev/null
+++ b/config/initializers/wrap_parameters.rb
@@ -0,0 +1,3 @@
+ActiveSupport.on_load(:action_controller) do
+ wrap_parameters format: [:json]
+end
\ No newline at end of file
diff --git a/config/puma.rb b/config/puma.rb
new file mode 100644
index 000000000..ad56a6a64
--- /dev/null
+++ b/config/puma.rb
@@ -0,0 +1,5 @@
+threads 1, 5
+port 3000
+environment ENV.fetch("RAILS_ENV") { "development" }
+pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
+plugin :tmp_restart
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 000000000..ddd43e639
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,3 @@
+Rails.application.routes.draw do
+ resources :charges, only: :index
+end
diff --git a/config/webpack/development.js b/config/webpack/development.js
new file mode 100644
index 000000000..c5edff94a
--- /dev/null
+++ b/config/webpack/development.js
@@ -0,0 +1,5 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development'
+
+const environment = require('./environment')
+
+module.exports = environment.toWebpackConfig()
diff --git a/config/webpack/environment.js b/config/webpack/environment.js
new file mode 100644
index 000000000..d16d9af74
--- /dev/null
+++ b/config/webpack/environment.js
@@ -0,0 +1,3 @@
+const { environment } = require('@rails/webpacker')
+
+module.exports = environment
diff --git a/config/webpack/test.js b/config/webpack/test.js
new file mode 100644
index 000000000..c5edff94a
--- /dev/null
+++ b/config/webpack/test.js
@@ -0,0 +1,5 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development'
+
+const environment = require('./environment')
+
+module.exports = environment.toWebpackConfig()
diff --git a/config/webpacker.yml b/config/webpacker.yml
new file mode 100644
index 000000000..cf2ab971e
--- /dev/null
+++ b/config/webpacker.yml
@@ -0,0 +1,25 @@
+default: &default
+ source_path: app/javascript
+ source_entry_path: packs
+ public_root_path: public
+ public_output_path: packs
+ cache_path: tmp/cache/webpacker
+ check_yarn_integrity: false
+ webpack_compile_output: true
+ resolved_paths: []
+ cache_manifest: false
+ extract_css: false
+ extensions:
+ - .js
+ - .css
+
+development:
+ <<: *default
+ compile: true
+ check_yarn_integrity: true
+
+
+test:
+ <<: *default
+ compile: true
+ public_output_path: packs-test
\ No newline at end of file
diff --git a/db/migrate/20210428000359_create_customer.rb b/db/migrate/20210428000359_create_customer.rb
new file mode 100644
index 000000000..080556e68
--- /dev/null
+++ b/db/migrate/20210428000359_create_customer.rb
@@ -0,0 +1,10 @@
+class CreateCustomer < ActiveRecord::Migration[6.0]
+ def change
+ create_table :customers do |t|
+ t.string :first_name, null: false
+ t.string :last_name, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20210428000508_create_charge.rb b/db/migrate/20210428000508_create_charge.rb
new file mode 100644
index 000000000..5287633f4
--- /dev/null
+++ b/db/migrate/20210428000508_create_charge.rb
@@ -0,0 +1,14 @@
+class CreateCharge < ActiveRecord::Migration[6.0]
+ def change
+ create_table :charges do |t|
+ t.boolean :paid, null: false
+ t.integer :amount, null: false
+ t.column :currency, 'char(3)', null: false
+ t.boolean :refunded, null: false
+ t.belongs_to :customer, foreign_key: true, null: false
+ t.timestamp :created, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 000000000..914a67903
--- /dev/null
+++ b/db/schema.rb
@@ -0,0 +1,38 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `rails
+# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 2021_04_28_000508) do
+
+ # These are extensions that must be enabled in order to support this database
+ enable_extension "plpgsql"
+
+ create_table "charges", force: :cascade do |t|
+ t.boolean "paid", null: false
+ t.integer "amount", null: false
+ t.string "currency", limit: 3, null: false
+ t.boolean "refunded", null: false
+ t.bigint "customer_id", null: false
+ t.datetime "created", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["customer_id"], name: "index_charges_on_customer_id"
+ end
+
+ create_table "customers", force: :cascade do |t|
+ t.string "first_name", null: false
+ t.string "last_name", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ end
+
+ add_foreign_key "charges", "customers"
+end
diff --git a/db/seeds.rb b/db/seeds.rb
new file mode 100644
index 000000000..23defc74f
--- /dev/null
+++ b/db/seeds.rb
@@ -0,0 +1,82 @@
+# Customer 1:
+# First Name: Johny
+# Last Name: Flow
+#
+# Customer 2:
+# First Name: Raj
+# Last Name: Jamnis
+#
+# Customer 3:
+# First Name: Andrew
+# Last Name: Chung
+#
+# Customer 4:
+# First Name: Mike
+# Last Name: Smith
+
+customers = [
+ { first_name: 'Johny', last_name: 'Flow' },
+ { first_name: 'Raj', last_name: 'Jamnis' },
+ { first_name: 'Andrew', last_name: 'Chung' },
+ { first_name: 'Mike', last_name: 'Smith' }
+]
+
+customers.each { |customer| Customer.create!(customer) }
+
+# 10 Should be successful transactions:
+# - 5 Should be linked to Customer 1
+# - 3 Should be linked to Customer 2
+# - 1 Should be linked to Customer 3
+# - 1 Should be linked to Customer 4
+#
+# 5 Should be transactions that failed:
+# - 3 Should be linked to Customer 3
+# - 2 Should be linked to Customer 4
+#
+# 5 should be disputed:
+# - 3 should be linked to Customer 1
+# - 2 should be linked to customer 2
+
+customers_ids = Customer.ids
+
+transactions = [
+ {
+ successful: [
+ { charges_amount: 5, customer_id: customers_ids[0] },
+ { charges_amount: 3, customer_id: customers_ids[1] },
+ { charges_amount: 1, customer_id: customers_ids[2] },
+ { charges_amount: 1, customer_id: customers_ids[3] }
+ ]
+ },
+ {
+ failed: [
+ { charges_amount: 3, customer_id: customers_ids[2] },
+ { charges_amount: 2, customer_id: customers_ids[3] }
+ ]
+ },
+ {
+ disputed: [
+ { charges_amount: 3, customer_id: customers_ids[0] },
+ { charges_amount: 2, customer_id: customers_ids[1] }
+ ]
+ }
+]
+
+charge_repo = ChargeRepo.new(model: Charge)
+
+transactions.each_with_index do |transactions_by_status, index|
+ transactions_by_status.each do |status, collection|
+ collection.each do |entity|
+ entity.fetch(:charges_amount).times do |number|
+ randomization_salt = (index + 1) * (number + 1)
+
+ charge_repo.create!(
+ customer_id: entity.fetch(:customer_id),
+ amount: rand((randomization_salt * 10)..(randomization_salt * 100)),
+ created: Time.now + rand((randomization_salt * 1)..(randomization_salt * 10)).hours,
+ status: status
+ )
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/assets/.keep b/lib/assets/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/tasks/.keep b/lib/tasks/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/log/.keep b/log/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..808dd0d78
--- /dev/null
+++ b/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "source",
+ "private": true,
+ "dependencies": {
+ "@rails/ujs": "^6.0.0",
+ "@rails/webpacker": "4.3.0",
+ "css-loader": "^5.2.4",
+ "css-minimizer-webpack-plugin": "^2.0.0",
+ "mini-css-extract-plugin": "^1.5.0"
+ },
+ "version": "0.1.0",
+ "devDependencies": {
+ "webpack-dev-server": "^3.11.2"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 000000000..aa5998a80
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,12 @@
+module.exports = {
+ plugins: [
+ require('postcss-import'),
+ require('postcss-flexbugs-fixes'),
+ require('postcss-preset-env')({
+ autoprefixer: {
+ flexbox: 'no-2009'
+ },
+ stage: 3
+ })
+ ]
+}
diff --git a/spec/factories/charges.rb b/spec/factories/charges.rb
new file mode 100644
index 000000000..a51eee144
--- /dev/null
+++ b/spec/factories/charges.rb
@@ -0,0 +1,22 @@
+FactoryBot.define do
+ factory :charge do
+ amount { Faker::Number.unique.number(digits: 5) }
+ currency { 'USD' }
+ created { Faker::Time.unique.between(from: Time.now - 1.day, to: Time.now) }
+
+ trait :successful do
+ paid { true }
+ refunded { false }
+ end
+
+ trait :failed do
+ paid { false }
+ refunded { true }
+ end
+
+ trait :disputed do
+ paid { false }
+ refunded { false }
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/factories/customers.rb b/spec/factories/customers.rb
new file mode 100644
index 000000000..4096c1782
--- /dev/null
+++ b/spec/factories/customers.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :customer do
+ first_name { Faker::Name.unique.first_name }
+ last_name { Faker::Name.unique.last_name }
+ end
+end
\ No newline at end of file
diff --git a/spec/features/charges/index_spec.rb b/spec/features/charges/index_spec.rb
new file mode 100644
index 000000000..03d345794
--- /dev/null
+++ b/spec/features/charges/index_spec.rb
@@ -0,0 +1,125 @@
+require 'rails_helper'
+
+def extract_customer_name(text)
+ text.gsub('Customer name: ', '').split(' ')
+end
+
+def extract_amount_number(text)
+ text.gsub('Amount: ', '').to_i
+end
+
+def extract_charge_date(text)
+ text.gsub('Charge date: ', '')
+end
+
+def extract_line_items(li)
+ li.find_all('p').to_a.map(&:text)
+end
+
+def transactions_text(id, context)
+ text = []
+
+ context.find(id).find_all('li').each do |li|
+ line_items = extract_line_items(li)
+ first_name, last_name = extract_customer_name(line_items[0])
+ amount = extract_amount_number(line_items[1])
+ created = extract_charge_date(line_items[2])
+
+ text << { first_name: first_name, last_name: last_name, amount: amount, created: created}
+ end
+
+ text
+end
+
+def transactions_data(charges)
+ charges.map do |record|
+ {
+ first_name: record.customer.first_name,
+ last_name: record.customer.last_name,
+ amount: record.amount,
+ created: record.created.strftime('%a %m/%d/%y%l:%M %p')
+ }
+ end
+end
+
+describe 'Charges index page' do
+ it 'has three tables' do
+ visit charges_path
+
+ successful = find('#successful-charges')
+ failed = find('#failed-charges')
+ disputed = find('#disputed-charges')
+
+ expect(failed).to have_content 'Failed Charges'
+ expect(successful).to have_content 'Successful Charges'
+ expect(disputed).to have_content 'Disputed Charges'
+ end
+
+ context 'with customers and charges' do
+ let(:customer) { create(:customer) }
+
+ let!(:failed_charges) do
+ charges = []
+ 5.times { charges << create(:charge, :failed, customer: customer) }
+ charges
+ end
+
+ let!(:disputed_charges) do
+ charges = []
+ 5.times { charges << create(:charge, :disputed, customer: customer) }
+ charges
+ end
+
+ let!(:successful_charges) do
+ charges = []
+ 10.times { charges << create(:charge, :successful, customer: customer) }
+ charges
+ end
+
+ it 'has 10 line items in successful list' do
+ visit charges_path
+
+ successful = find('#successful-charges')
+ line_items_count = successful.find_all('li').count
+
+ successful_text = transactions_text('#successful-charges', self)
+ successful_text.sort_by { |text| text[:amount] }
+
+ successful_data = transactions_data(successful_charges)
+ successful_data.sort_by { |data| data[:amount] }
+
+ expect(successful_text).to match_array(successful_data)
+ expect(line_items_count).to eq 10
+ end
+
+ it 'has 5 failed charges in failed charges list' do
+ visit charges_path
+
+ failed_text = transactions_text('#failed-charges', self)
+ failed_text.sort_by { |text| text[:amount] }
+
+ failed_data = transactions_data(failed_charges)
+ failed_data.sort_by { |data| data[:amount] }
+
+ expect(failed_data).to match_array(failed_text)
+ end
+
+ it "doesn't have failed data in disputed data" do
+ visit charges_path
+
+ disputed_text = transactions_text('#disputed-charges', self)
+ failed_text = transactions_text('#failed-charges', self)
+
+ disputed_text.sort_by { |text| text[:amount] }
+ failed_text.sort_by { |text| text[:amount] }
+
+ failed_data = transactions_data(failed_charges)
+ disputed_data = transactions_data(disputed_charges)
+
+ failed_data.sort_by { |data| data[:amount] }
+ disputed_data.sort_by { |data| data[:amount] }
+
+ expect(failed_data).to_not include(disputed_text)
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 000000000..99301502d
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,35 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'spec_helper'
+ENV['RAILS_ENV'] ||= 'test'
+require File.expand_path('../config/environment', __dir__)
+# Prevent database truncation if the environment is production
+abort("The Rails environment is running in production mode!") if Rails.env.production?
+require 'rspec/rails'
+require 'capybara/rspec'
+require 'database_cleaner/active_record'
+
+begin
+ ActiveRecord::Migration.maintain_test_schema!
+rescue ActiveRecord::PendingMigrationError => e
+ puts e.to_s.strip
+ exit 1
+end
+
+
+RSpec.configure do |config|
+ config.use_transactional_fixtures = false
+ config.before(:suite) { DatabaseCleaner.clean_with(:truncation) }
+ config.before(:each) { DatabaseCleaner.strategy = :transaction }
+ config.before(:each) { DatabaseCleaner.start }
+ config.after(:each) { DatabaseCleaner.clean }
+ config.before(:suite) { Capybara.server = :puma }
+ config.include FactoryBot::Syntax::Methods
+
+
+ # The different available types are documented in the features, such as in
+ # https://relishapp.com/rspec/rspec-rails/docs
+ config.infer_spec_type_from_file_location!
+
+ # Filter lines from Rails gems in backtraces.
+ config.filter_rails_from_backtrace!
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 000000000..06e09a309
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,11 @@
+RSpec.configure do |config|
+ config.expect_with :rspec do |expectations|
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ config.mock_with :rspec do |mocks|
+ mocks.verify_partial_doubles = true
+ end
+
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+end
diff --git a/tmp/.keep b/tmp/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/tmp/pids/.keep b/tmp/pids/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/vendor/.keep b/vendor/.keep
new file mode 100644
index 000000000..e69de29bb