Merge pull request #31 from ZeusWPI/request-payments

Request payments
This commit is contained in:
benji 2017-01-11 14:56:01 +01:00 committed by GitHub
commit 74aa636ae5
36 changed files with 440 additions and 36 deletions

View file

@ -1 +1 @@
2.2.2 2.3.1

View file

@ -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/

View file

@ -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/

View file

@ -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/

View file

@ -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/

View file

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

View file

@ -5,5 +5,4 @@ class PagesController < ApplicationController
def landing def landing
@statistics = Statistics.new @statistics = Statistics.new
end end
end end

View file

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

View file

@ -9,14 +9,24 @@ class TransactionsController < ApplicationController
def create def create
@transaction = Transaction.new(transaction_params) @transaction = Transaction.new(transaction_params)
@transaction.reverse if @transaction.amount < 0 @transaction.reverse if @transaction.amount < 0
authorize!(:create, @transaction)
if can? :create, @transaction
if @transaction.save if @transaction.save
render json: @transaction, status: :created render json: @transaction, status: :created
else else
render json: @transaction.errors.full_messages, render json: @transaction.errors.full_messages,
status: :unprocessable_entity status: :unprocessable_entity
end end
else
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 end
private private

View file

@ -0,0 +1,2 @@
module NotificationsHelper
end

View file

@ -0,0 +1,2 @@
module RequestsHelper
end

View file

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

View file

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

View file

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

48
app/models/request.rb Normal file
View file

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

View file

@ -15,22 +15,13 @@
# #
class Transaction < ActiveRecord::Base class Transaction < ActiveRecord::Base
belongs_to :debtor, class_name: 'User' include BaseTransaction
belongs_to :creditor, class_name: 'User' include TransactionHelpers
belongs_to :issuer, polymorphic: true
after_save :recalculate_balances after_save :recalculate_balances!
after_destroy :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? 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) def signed_amount_for(user)
return -amount if user == debtor return -amount if user == debtor
@ -44,18 +35,8 @@ class Transaction < ActiveRecord::Base
private private
def recalculate_balances def recalculate_balances!
creditor.calculate_balance! creditor.calculate_balance!
debtor.calculate_balance! debtor.calculate_balance!
end 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 end

View file

@ -18,6 +18,11 @@ class User < ActiveRecord::Base
class_name: 'Transaction', foreign_key: 'creditor_id' class_name: 'Transaction', foreign_key: 'creditor_id'
has_many :outgoing_transactions, has_many :outgoing_transactions,
class_name: 'Transaction', foreign_key: 'debtor_id' 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' has_many :issued_transactions, as: :issuer, class_name: 'Transaction'
@ -42,7 +47,7 @@ class User < ActiveRecord::Base
end end
def self.zeus def self.zeus
find_or_create_by name: 'Zeus' @@zeus ||= find_or_create_by name: 'Zeus'
end end
end end

View file

@ -6,6 +6,8 @@ class UserAbility
can :manage, :all if user.penning? 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| can :create, Transaction do |t|
t.debtor == user && t.amount <= Rails.application.config.maximum_amount t.debtor == user && t.amount <= Rails.application.config.maximum_amount
end end

View file

@ -7,3 +7,11 @@
- if current_user.penning - if current_user.penning
%li.pure-menu-item %li.pure-menu-item
=link_to "Zeus", User.zeus, class: "pure-menu-link" =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'

View file

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

View file

@ -0,0 +1,2 @@
= render 'index', actions: true, title: 'Unread Notifications', notifications: @notifications[false]
= render 'index', actions: false, title: 'Read Notifications', notifications: @notifications[true]

View file

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

View file

@ -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']

View file

@ -13,7 +13,7 @@
%span.input-group-addon %span.input-group-addon
%span.glyphicon.glyphicon-euro %span.glyphicon.glyphicon-euro
= f.number_field :euros, value: amount(@transaction.amount), = 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, class: "form-control", size: 20, required: true,
max: (Rails.application.config.maximum_amount/100 unless current_user.penning) max: (Rails.application.config.maximum_amount/100 unless current_user.penning)
= f.submit "Send it!", class: "pure-button pure-button-primary btn" = f.submit "Send it!", class: "pure-button pure-button-primary btn"

View file

@ -6,7 +6,15 @@ Rails.application.routes.draw do
root to: 'pages#landing' root to: 'pages#landing'
resources :transactions, only: [:index, :create] 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" get 'datatables/:id' => 'datatables#transactions_for_user', as: "user_transactions"
end end

View file

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

View file

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

View file

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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| create_table "clients", force: :cascade do |t|
t.string "name", null: false 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", ["key"], name: "index_clients_on_key"
add_index "clients", ["name"], name: "index_clients_on_name" 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| create_table "transactions", force: :cascade do |t|
t.integer "debtor_id", null: false t.integer "debtor_id", null: false
t.integer "creditor_id", null: false t.integer "creditor_id", null: false

View file

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe NotificationsController, type: :controller do
end

View file

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe RequestsController, type: :controller do
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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