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 %>

+ + +
\ 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