diff --git a/.ruby-version b/.ruby-version index b1b25a5..2bf1c1c 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.2.2 +2.3.1 diff --git a/app/assets/javascripts/notifications.coffee b/app/assets/javascripts/notifications.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/notifications.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/requests.coffee b/app/assets/javascripts/requests.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/requests.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/notifications.scss b/app/assets/stylesheets/notifications.scss new file mode 100644 index 0000000..c7ddf91 --- /dev/null +++ b/app/assets/stylesheets/notifications.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the notifications controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/requests.scss b/app/assets/stylesheets/requests.scss new file mode 100644 index 0000000..45c818a --- /dev/null +++ b/app/assets/stylesheets/requests.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the requests controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 0000000..9f86424 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,21 @@ +class NotificationsController < ApplicationController + load_and_authorize_resource :user, only: :index + + before_action :load_notification, only: :read + authorize_resource :notification, only: :read + + def index + @notifications = @user.notifications.group_by(&:read) + end + + def read + @notification.read! + redirect_to user_notifications_path(@notification.user) + end + + private + + def load_notification + @notification = Notification.find params[:notification_id] + end +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index fd7c718..42b9a09 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -5,5 +5,4 @@ class PagesController < ApplicationController def landing @statistics = Statistics.new end - end diff --git a/app/controllers/requests_controller.rb b/app/controllers/requests_controller.rb new file mode 100644 index 0000000..57d68ad --- /dev/null +++ b/app/controllers/requests_controller.rb @@ -0,0 +1,26 @@ +class RequestsController < ApplicationController + load_and_authorize_resource :user, only: :index + + before_action :load_request, only: [:confirm, :decline] + authorize_resource :request, only: [:confirm, :decline] + + def index + @requests = @user.incoming_requests.group_by(&:status) + end + + def confirm + @request.confirm! + redirect_to user_requests_path(@request.debtor) + end + + def decline + @request.decline! + redirect_to user_requests_path(@request.debtor) + end + + private + + def load_request + @request = Request.find params[:request_id] + end +end diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index 2aad2b2..d4d1a45 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -9,13 +9,23 @@ class TransactionsController < ApplicationController def create @transaction = Transaction.new(transaction_params) @transaction.reverse if @transaction.amount < 0 - authorize!(:create, @transaction) - if @transaction.save - render json: @transaction, status: :created + if can? :create, @transaction + if @transaction.save + render json: @transaction, status: :created + else + render json: @transaction.errors.full_messages, + status: :unprocessable_entity + end else - render json: @transaction.errors.full_messages, - status: :unprocessable_entity + request = Request.new @transaction.info + authorize!(:create, request) + if request.save + render json: request, status: :created + else + render json: request.errors.full_messages, + status: :unprocessable_entity + end end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb new file mode 100644 index 0000000..7342393 --- /dev/null +++ b/app/helpers/notifications_helper.rb @@ -0,0 +1,2 @@ +module NotificationsHelper +end diff --git a/app/helpers/requests_helper.rb b/app/helpers/requests_helper.rb new file mode 100644 index 0000000..53ac95c --- /dev/null +++ b/app/helpers/requests_helper.rb @@ -0,0 +1,2 @@ +module RequestsHelper +end diff --git a/app/models/concerns/base_transaction.rb b/app/models/concerns/base_transaction.rb new file mode 100644 index 0000000..31c11dc --- /dev/null +++ b/app/models/concerns/base_transaction.rb @@ -0,0 +1,31 @@ +module BaseTransaction + extend ActiveSupport::Concern + include ActionView::Helpers::NumberHelper + + included do + belongs_to :debtor, class_name: 'User' + belongs_to :creditor, class_name: 'User' + belongs_to :issuer, polymorphic: true + + validates :amount, numericality: { greater_than: 0 } + validate :different_debtor_creditor + end + + def info + attributes.symbolize_keys.extract!( + :debtor_id, :creditor_id, :issuer_id, :issuer_type, :message, :amount + ) + end + + def amount_f + number_to_currency amount/100.0, unit: '€' + end + + private + + def different_debtor_creditor + if self.debtor == self.creditor + self.errors.add :base, "Can't write money to yourself" + end + end +end diff --git a/app/models/concerns/transaction_helpers.rb b/app/models/concerns/transaction_helpers.rb new file mode 100644 index 0000000..7187ee9 --- /dev/null +++ b/app/models/concerns/transaction_helpers.rb @@ -0,0 +1,12 @@ +module TransactionHelpers + include ActiveSupport::Concern + + def peer_of(user) + return creditor if user == debtor + return debtor if user == creditor + end + + def is_client_transaction? + issuer_type == 'Client' + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 0000000..a926d62 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: notifications +# +# id :integer not null, primary key +# user_id :integer not null +# message :string +# read :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# + +class Notification < ActiveRecord::Base + belongs_to :user + + def read! + update_attributes read: true + end +end diff --git a/app/models/request.rb b/app/models/request.rb new file mode 100644 index 0000000..9e85688 --- /dev/null +++ b/app/models/request.rb @@ -0,0 +1,48 @@ +# == Schema Information +# +# Table name: requests +# +# id :integer not null, primary key +# debtor_id :integer not null +# creditor_id :integer not null +# issuer_id :integer not null +# issuer_type :string not null +# amount :integer default(0), not null +# message :string +# status :integer default(0) +# created_at :datetime not null +# updated_at :datetime not null +# + +class Request < ActiveRecord::Base + include BaseTransaction + + enum status: [:open, :confirmed, :declined] + + def confirm! + return unless open? + + Transaction.create info + Notification.create user: creditor,message: confirmed_message + + update_attributes status: :confirmed + end + + def decline! + return unless open? + + Notification.create user: creditor, message: declined_message + + update_attributes status: :declined + end + + private + + def confirmed_message + "Your request for €#{amount/100.0} for \"#{message}\" has been accepted by #{debtor.name}." + end + + def declined_message + "#{debtor.name} refuses to pay €#{amount/100.0} for \"#{message}\"." + end +end diff --git a/app/models/transaction.rb b/app/models/transaction.rb index 7d82df8..42c2037 100644 --- a/app/models/transaction.rb +++ b/app/models/transaction.rb @@ -15,22 +15,13 @@ # class Transaction < ActiveRecord::Base - belongs_to :debtor, class_name: 'User' - belongs_to :creditor, class_name: 'User' - belongs_to :issuer, polymorphic: true + include BaseTransaction + include TransactionHelpers - after_save :recalculate_balances - after_destroy :recalculate_balances + after_save :recalculate_balances! + after_destroy :recalculate_balances! - validates :amount, numericality: { greater_than: 0 } validates :id_at_client, presence: true, uniqueness: { scope: :issuer_id }, if: :is_client_transaction? - validate :different_debtor_creditor - - - def peer_of(user) - return creditor if user == debtor - return debtor if user == creditor - end def signed_amount_for(user) return -amount if user == debtor @@ -44,18 +35,8 @@ class Transaction < ActiveRecord::Base private - def recalculate_balances + def recalculate_balances! creditor.calculate_balance! debtor.calculate_balance! end - - def different_debtor_creditor - if self.debtor == self.creditor - self.errors.add :base, "Can't write money to yourself" - end - end - - def is_client_transaction? - issuer_type == "Client" - end end diff --git a/app/models/user.rb b/app/models/user.rb index d3c234b..df5aa07 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -18,6 +18,11 @@ class User < ActiveRecord::Base class_name: 'Transaction', foreign_key: 'creditor_id' has_many :outgoing_transactions, class_name: 'Transaction', foreign_key: 'debtor_id' + has_many :incoming_requests, + class_name: 'Request', foreign_key: 'debtor_id' + has_many :outgoing_requests, + class_name: 'Request', foreign_key: 'debtor_id' + has_many :notifications has_many :issued_transactions, as: :issuer, class_name: 'Transaction' @@ -42,7 +47,7 @@ class User < ActiveRecord::Base end def self.zeus - find_or_create_by name: 'Zeus' + @@zeus ||= find_or_create_by name: 'Zeus' end end diff --git a/app/models/user_ability.rb b/app/models/user_ability.rb index 196d854..1574f44 100644 --- a/app/models/user_ability.rb +++ b/app/models/user_ability.rb @@ -5,7 +5,9 @@ class UserAbility return unless user can :manage, :all if user.penning? - can :read, user, id: user.id + can :read, user, id: user.id + can :manage, Request, creditor_id: user.id + can :manage, Notification, user_id: user.id can :create, Transaction do |t| t.debtor == user && t.amount <= Rails.application.config.maximum_amount end diff --git a/app/views/application/_menu.html.haml b/app/views/application/_menu.html.haml index 4de3be6..09fe0ac 100644 --- a/app/views/application/_menu.html.haml +++ b/app/views/application/_menu.html.haml @@ -7,3 +7,11 @@ - if current_user.penning %li.pure-menu-item =link_to "Zeus", User.zeus, class: "pure-menu-link" + %li.pure-menu-item + = link_to "Requests (#{User.zeus.incoming_requests.size})", user_requests_path(User.zeus), class: 'pure-menu-link' + %li.pure-menu-item + = link_to "Notifications (#{User.zeus.notifications.size})", user_notifications_path(User.zeus), class: 'pure-menu-link' + %li.pure-menu-item + = link_to "Requests (#{current_user.incoming_requests.size})", user_requests_path(current_user), class: 'pure-menu-link' + %li.pure-menu-item + = link_to "Notifications (#{current_user.notifications.size})", user_notifications_path(current_user), class: 'pure-menu-link' diff --git a/app/views/notifications/_index.html.haml b/app/views/notifications/_index.html.haml new file mode 100644 index 0000000..7d52c26 --- /dev/null +++ b/app/views/notifications/_index.html.haml @@ -0,0 +1,15 @@ +%h4= title +%table.pure-table + %thead + %tr + %th Message + - if actions + %th Mark as read + %tbody + - (notifications || []).each do |n| + %tr + %td= n.message + - if actions + %td + = link_to notification_read_path(n), method: :post do + %span.glyphicon.glyphicon-ok diff --git a/app/views/notifications/index.html.haml b/app/views/notifications/index.html.haml new file mode 100644 index 0000000..15012ea --- /dev/null +++ b/app/views/notifications/index.html.haml @@ -0,0 +1,2 @@ += render 'index', actions: true, title: 'Unread Notifications', notifications: @notifications[false] += render 'index', actions: false, title: 'Read Notifications', notifications: @notifications[true] diff --git a/app/views/requests/_index.html.haml b/app/views/requests/_index.html.haml new file mode 100644 index 0000000..3902a64 --- /dev/null +++ b/app/views/requests/_index.html.haml @@ -0,0 +1,25 @@ +%h4= title +%table.pure-table + %thead + %tr + %th Peer + %th Issuer + %th Amount + %th Message + - if actions + %th Accept + %th Decline + %tbody + - (requests || []).each do |r| + %tr + %td= r.creditor.name + %td= r.issuer.name + %td= r.amount_f + %td= r.message + - if actions + %td + = link_to request_confirm_path(r), method: :post do + %span.glyphicon.glyphicon-ok + %td + = link_to request_decline_path(r), method: :post do + %span.glyphicon.glyphicon-remove diff --git a/app/views/requests/index.html.haml b/app/views/requests/index.html.haml new file mode 100644 index 0000000..e56d57c --- /dev/null +++ b/app/views/requests/index.html.haml @@ -0,0 +1,3 @@ += render 'index', actions: true, title: 'Open Requests', requests: @requests['open'] += render 'index', actions: false, title: 'Confirmed Requests', requests: @requests['confirmed'] += render 'index', actions: false, title: 'Declined Requests', requests: @requests['declined'] diff --git a/app/views/transactions/_new.html.haml b/app/views/transactions/_new.html.haml index cb4be2c..9b2a6db 100644 --- a/app/views/transactions/_new.html.haml +++ b/app/views/transactions/_new.html.haml @@ -13,7 +13,7 @@ %span.input-group-addon %span.glyphicon.glyphicon-euro = f.number_field :euros, value: amount(@transaction.amount), - placeholder: "Amount", step: 0.01, min: (0.01 unless current_user.penning), + placeholder: "Amount", step: 0.01, class: "form-control", size: 20, required: true, max: (Rails.application.config.maximum_amount/100 unless current_user.penning) = f.submit "Send it!", class: "pure-button pure-button-primary btn" diff --git a/config/routes.rb b/config/routes.rb index e4d543e..bf9a17a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,7 +6,15 @@ Rails.application.routes.draw do root to: 'pages#landing' resources :transactions, only: [:index, :create] - resources :users, only: [:show, :index] + resources :users, only: [:index, :show] do + resources :requests, only: [:index], shallow: true do + post :confirm + post :decline + end + resources :notifications, only: [:index], shallow: true do + post :read + end + end get 'datatables/:id' => 'datatables#transactions_for_user', as: "user_transactions" end diff --git a/db/migrate/20170109123717_create_requests.rb b/db/migrate/20170109123717_create_requests.rb new file mode 100644 index 0000000..1f1eca3 --- /dev/null +++ b/db/migrate/20170109123717_create_requests.rb @@ -0,0 +1,18 @@ +class CreateRequests < ActiveRecord::Migration + def change + create_table :requests do |t| + t.references :debtor, index: true, null: false + t.references :creditor, index: true, null: false + t.references :issuer, polymorphic: true, index: true, null: false + t.integer :amount, null: false, default: 0 + t.string :message + + t.integer :status, default: 0 + + t.timestamps null: false + end + + add_foreign_key :request, :users, column: :creditor_id + add_foreign_key :request, :users, column: :debtor_id + end +end diff --git a/db/migrate/20170109150245_create_notifications.rb b/db/migrate/20170109150245_create_notifications.rb new file mode 100644 index 0000000..b8b3717 --- /dev/null +++ b/db/migrate/20170109150245_create_notifications.rb @@ -0,0 +1,11 @@ +class CreateNotifications < ActiveRecord::Migration + def change + create_table :notifications do |t| + t.references :user, index: true, null: false + t.string :message + t.boolean :read, default: false + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d784c74..3441a84 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150914095049) do +ActiveRecord::Schema.define(version: 20170109150245) do create_table "clients", force: :cascade do |t| t.string "name", null: false @@ -23,6 +23,32 @@ ActiveRecord::Schema.define(version: 20150914095049) do add_index "clients", ["key"], name: "index_clients_on_key" add_index "clients", ["name"], name: "index_clients_on_name" + create_table "notifications", force: :cascade do |t| + t.integer "user_id", null: false + t.string "message" + t.boolean "read", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "notifications", ["user_id"], name: "index_notifications_on_user_id" + + create_table "requests", force: :cascade do |t| + t.integer "debtor_id", null: false + t.integer "creditor_id", null: false + t.integer "issuer_id", null: false + t.string "issuer_type", null: false + t.integer "amount", default: 0, null: false + t.string "message" + t.integer "status", default: 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "requests", ["creditor_id"], name: "index_requests_on_creditor_id" + add_index "requests", ["debtor_id"], name: "index_requests_on_debtor_id" + add_index "requests", ["issuer_type", "issuer_id"], name: "index_requests_on_issuer_type_and_issuer_id" + create_table "transactions", force: :cascade do |t| t.integer "debtor_id", null: false t.integer "creditor_id", null: false diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb new file mode 100644 index 0000000..4212687 --- /dev/null +++ b/spec/controllers/notifications_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe NotificationsController, type: :controller do + +end diff --git a/spec/controllers/requests_controller_spec.rb b/spec/controllers/requests_controller_spec.rb new file mode 100644 index 0000000..f5fc12f --- /dev/null +++ b/spec/controllers/requests_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe RequestsController, type: :controller do + +end diff --git a/spec/factories/notifications.rb b/spec/factories/notifications.rb new file mode 100644 index 0000000..20df5f0 --- /dev/null +++ b/spec/factories/notifications.rb @@ -0,0 +1,18 @@ +# == Schema Information +# +# Table name: notifications +# +# id :integer not null, primary key +# user_id :integer not null +# message :string +# read :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryGirl.define do + factory :notification do + + end + +end diff --git a/spec/factories/requests.rb b/spec/factories/requests.rb new file mode 100644 index 0000000..8193437 --- /dev/null +++ b/spec/factories/requests.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: requests +# +# id :integer not null, primary key +# debtor_id :integer not null +# creditor_id :integer not null +# issuer_id :integer not null +# issuer_type :string not null +# amount :integer default(0), not null +# message :string +# status :integer default(0) +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryGirl.define do + factory :request do + + end + +end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb new file mode 100644 index 0000000..1473b13 --- /dev/null +++ b/spec/helpers/notifications_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the NotificationsHelper. For example: +# +# describe NotificationsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe NotificationsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/requests_helper_spec.rb b/spec/helpers/requests_helper_spec.rb new file mode 100644 index 0000000..a2ab326 --- /dev/null +++ b/spec/helpers/requests_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the RequestsHelper. For example: +# +# describe RequestsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe RequestsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb new file mode 100644 index 0000000..964cd26 --- /dev/null +++ b/spec/models/notification_spec.rb @@ -0,0 +1,17 @@ +# == Schema Information +# +# Table name: notifications +# +# id :integer not null, primary key +# user_id :integer not null +# message :string +# read :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# + +require 'rails_helper' + +RSpec.describe Notification, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/request_spec.rb b/spec/models/request_spec.rb new file mode 100644 index 0000000..1aab9ee --- /dev/null +++ b/spec/models/request_spec.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: requests +# +# id :integer not null, primary key +# debtor_id :integer not null +# creditor_id :integer not null +# issuer_id :integer not null +# issuer_type :string not null +# amount :integer default(0), not null +# message :string +# status :integer default(0) +# created_at :datetime not null +# updated_at :datetime not null +# + +require 'rails_helper' + +RSpec.describe Request, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end