From a44e022bfa724bff6021bdf58da03dfa127a5d42 Mon Sep 17 00:00:00 2001 From: Michael Arnoldus Date: Sun, 26 Nov 2017 14:09:26 +0100 Subject: [PATCH 1/4] Created functionality that will generate a list of archived members with funds and take that list and transfer the funds to the group account --- app/controllers/allocations_controller.rb | 16 +--------------- app/models/group.rb | 22 ++++++++++++++++++++++ app/models/membership.rb | 17 +++++++++++++++++ app/services/allocation_service.rb | 21 +++++++++++++++++++++ 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/app/controllers/allocations_controller.rb b/app/controllers/allocations_controller.rb index 51589e8..9401a12 100644 --- a/app/controllers/allocations_controller.rb +++ b/app/controllers/allocations_controller.rb @@ -25,25 +25,11 @@ def upload_review api :POST, '/allocations?membership_id&amount' def create group = Group.find(allocation_params[:group_id]) - user = User.find(allocation_params[:user_id]) - amount = allocation_params[:amount] notify = allocation_params[:notify] params[:allocation].delete(:notify) render status: 403, nothing: true and return unless current_user.is_admin_for?(group) - allocation = Allocation.new(allocation_params) - if allocation.save - m = Membership.find_by(group_id: allocation_params[:group_id], member_id: allocation_params[:user_id]) - Transaction.create!({ - datetime: allocation.created_at, - from_account_id: m.incoming_account_id, - to_account_id: m.status_account_id, - user_id: current_user.id, - amount: amount - }) - if notify && (amount > 0) - UserMailer.notify_member_that_they_received_allocation(admin: current_user, member: user, group: group, amount: amount).deliver_later - end + if (allocation = AllocationService.create_allocation(allocation_params, notify, current_user)) render json: [allocation], status: 201 else render status: 400, nothing: true diff --git a/app/models/group.rb b/app/models/group.rb index 5cb73c9..e162a38 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -76,6 +76,28 @@ def ensure_group_account_exist() group_membership end + def find_archived_members_with_funds() + l = [] + Membership.where(group_id: id).where.not(archived_at: nil).find_each do |membership| + if membership.raw_balance != 0 + l.push({ + membership_id: membership.id, + user_name: User.find(membership.member_id).name, + archived_at: membership.archived_at, + balance: membership.raw_balance + }) + end + end + l + end + + def transfer_memberships_to_group_account(transfer_from_list, current_user) + group_membership = ensure_group_account_exist + transfer_from_list.each do |e| + Membership.find(e[:membership_id]).transfer_funds_to_membership(group_membership, current_user) + end + end + def add_member(user) memberships.create!(member: user, is_admin: false) end diff --git a/app/models/membership.rb b/app/models/membership.rb index 78b92d9..2132bcf 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -78,6 +78,23 @@ def transactions_data_ok? (balance_on_status_account == raw_balance) && (balance_on_incoming_account + total_allocations == 0) end + # This function will transfer all funds to a different membership + def transfer_funds_to_membership(receiver, current_user) + amount = raw_balance + ActiveRecord::Base.transaction do + a = Allocation.create(user_id: member_id, group_id: group_id, amount: -amount) + Allocation.create(user_id: receiver.member_id, group_id: receiver.group_id, amount: amount, + created_at: a.created_at, updated_at: a.updated_at) + Transaction.create!({ + datetime: a.created_at, + from_account_id: status_account_id, + to_account_id: receiver.status_account_id, + user_id: current_user.id, + amount: amount + }) + end + end + private def currency_code group.currency_code diff --git a/app/services/allocation_service.rb b/app/services/allocation_service.rb index 3c3358f..6e284b7 100644 --- a/app/services/allocation_service.rb +++ b/app/services/allocation_service.rb @@ -25,6 +25,27 @@ def self.check_csv_for_errors(csv:, group:) errors if errors.any? end + def self.create_allocation(allocation_params, notify, current_user) + allocation = Allocation.new(allocation_params) + amount = allocation_params[:amount] + if allocation.save + m = Membership.find_by(group_id: allocation_params[:group_id], member_id: allocation_params[:user_id]) + Transaction.create!({ + datetime: allocation.created_at, + from_account_id: m.incoming_account_id, + to_account_id: m.status_account_id, + user_id: current_user.id, + amount: amount + }) + if notify && (amount > 0) + UserMailer.notify_member_that_they_received_allocation(admin: current_user, member: user, group: group, amount: amount).deliver_later + end + allocation + else + nil + end + end + def self.generate_csv_upload_preview(csv:, group:) csv.group_by { |row| row[0].downcase }.map do |email, rows| allocation_amount = rows.sum { |row| row[1].to_f } From c85d34b6cbc844b602863d63a41a295395c3b03d Mon Sep 17 00:00:00 2001 From: Michael Arnoldus Date: Sun, 26 Nov 2017 17:24:42 +0100 Subject: [PATCH 2/4] Send list to admins with archived members with funds. Test added. --- app/mailers/user_mailer.rb | 9 ++++ app/models/group.rb | 22 ++++++++ .../notify_admins_archived_member_funds.erb | 23 +++++++++ ...ins_funds_is_returned_to_group_account.erb | 3 +- spec/models/group_spec.rb | 50 +++++++++++++++++++ 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 app/views/user_mailer/notify_admins_archived_member_funds.erb diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index f58a8a3..6cdb21e 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -95,6 +95,15 @@ def notify_admins_funds_is_returned_to_group_account(admin:, bucket:, done_by:, subject: "Funds from cancelled bucket is returned to group account") end + def notify_admins_archived_member_funds(admin: , group: ,memberlist: ) + @memberlist = memberlist + @group = group + @group_user = group.ensure_group_user_exist() + mail(to: admin.name_and_email, + from: "Cobudget Updates ", + subject: "Funds from archived members is returned to group account") + end + def check_transactions_email mail(to: "devops@greaterthan.finance", from: "Cobudget Updates ", diff --git a/app/models/group.rb b/app/models/group.rb index e162a38..c22004c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -96,6 +96,28 @@ def transfer_memberships_to_group_account(transfer_from_list, current_user) transfer_from_list.each do |e| Membership.find(e[:membership_id]).transfer_funds_to_membership(group_membership, current_user) end + mail_admins_about_members(transfer_from_list) + end + + def for_each_admin + Membership.where(group_id: id, is_admin: :true).find_each do |admin| + yield admin + end + end + + def mail_admins_about_members(memberlist) + l = memberlist.map { |e| + e[:archived_at] = e[:archived_at].strftime("%B %d, %Y") + e[:balance] = %(#{e[:balance]} #{currency_symbol}) + } + for_each_admin do |admin| + UserMailer.notify_admins_archived_member_funds(admin: admin.member, group: self, memberlist: memberlist).deliver_later + end + end + + def cleanup_archived_members_with_funds(current_user) + l = find_archived_members_with_funds() + transfer_memberships_to_group_account(l, current_user) end def add_member(user) diff --git a/app/views/user_mailer/notify_admins_archived_member_funds.erb b/app/views/user_mailer/notify_admins_archived_member_funds.erb new file mode 100644 index 0000000..2a09226 --- /dev/null +++ b/app/views/user_mailer/notify_admins_archived_member_funds.erb @@ -0,0 +1,23 @@ +

+ Archived members with funds has been found in <%= @group.name %> +

+ +

+ The funds has been transferred to the group account <%= @group_user.name %> which you will find in the list of funders. +

+ + + + + + + + + <% @memberlist.each do |m| %> + + + + + + <% end %> +
Archived members with funds
UserArchivedAmount
<%= m[:user_name] %><%= m[:archived_at] %><%= m[:balance] %>
diff --git a/app/views/user_mailer/notify_admins_funds_is_returned_to_group_account.erb b/app/views/user_mailer/notify_admins_funds_is_returned_to_group_account.erb index 77eb4f4..fd7a154 100644 --- a/app/views/user_mailer/notify_admins_funds_is_returned_to_group_account.erb +++ b/app/views/user_mailer/notify_admins_funds_is_returned_to_group_account.erb @@ -6,7 +6,8 @@

A previous member <%= @archived_member.name %>, had funded the bucket with <%= @formatted_amount %>. As this person is no longer a member of the group, the funds - has been returned to the group account <%= @group_account.name %>. + has been returned to the group account <%= @group_account.name %> which you will + find in the list of funders.

<%= link_to 'view', "#{root_url}#/buckets/#{@bucket.id}" %>

diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 99e4a92..9b230f5 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' RSpec.describe Group, :type => :model do + after { ActionMailer::Base.deliveries.clear } describe "#add_admin(user)" do context "user already member of group" do it "makes the user an admin of group" do @@ -65,4 +66,53 @@ end end end + + describe "membership cleanup" do + context "by moving funds from archived member to group account" do + before do + admins = create_list(:user, 2) + admins.each { |a| group.add_admin(a) } + normal_users = create_list(:user, 3) + @normal_memberships = normal_users.map { |u| group.add_member(u) } + normal_users.each { |u| create(:allocation, user: u, group: group, amount: 10) } + @normal_memberships.each { |m| create(:transaction, from_account_id: m.incoming_account_id, + to_account_id: m.status_account_id, user_id: user.id, amount: 10) } + archived_users = create_list(:user, 3) + @archived_memberships = archived_users.map { |u| group.add_member(u) } + archived_users.each { |u| create(:allocation, user: u, group: group, amount: 7) } + @archived_memberships.each { |m| create(:transaction, from_account_id: m.incoming_account_id, + to_account_id: m.status_account_id, user_id: user.id, amount: 7) } + @archived_memberships.each { |m| m.archive! } + group.cleanup_archived_members_with_funds(group.ensure_group_user_exist()) + @group_membership = group.ensure_group_account_exist() + end + + it "has moved funds from archived users" do + @archived_memberships.each do |m| + expect(m.raw_balance).to eq(0) + expect(Account.find(m.status_account_id).balance).to eq(0) + end + end + + it "has not touched funds in non-archived users" do + @normal_memberships.each do |m| + expect(m.raw_balance).to eq(10) + expect(Account.find(m.status_account_id).balance).to eq(10) + end + end + + it "has moved the funds from echived users to group account" do + expect(@group_membership.raw_balance).to eq(21) + expect(Account.find(@group_membership.status_account_id).balance).to eq(21) + end + + it "sends mails to admins" do + sent_emails = ActionMailer::Base.deliveries + recipients = sent_emails.map { |email| email.to.first } + admin_emails = Membership.where(group_id: group.id, is_admin: :true).map { |admin| admin.member.email } + expect(recipients).to match_array(admin_emails) + expect(sent_emails.first.body).to include("transferred") + end + end + end end From 541920dac1a8ccd7614027e4ed195d4873141bbc Mon Sep 17 00:00:00 2001 From: Michael Arnoldus Date: Sun, 26 Nov 2017 17:59:01 +0100 Subject: [PATCH 3/4] Clean up archived members with funds once per day --- app/extras/cobudget_cleanup.rb | 13 +++++++++++++ app/models/group.rb | 2 +- lib/tasks/cobudget.rake | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 app/extras/cobudget_cleanup.rb diff --git a/app/extras/cobudget_cleanup.rb b/app/extras/cobudget_cleanup.rb new file mode 100644 index 0000000..2e0cd6e --- /dev/null +++ b/app/extras/cobudget_cleanup.rb @@ -0,0 +1,13 @@ +class CobudgetCleanup + def self.archived_members_with_funds! + if DateTime.now.utc.hour == 7 + Group.find_each do |g| + l = g.find_archived_members_with_funds() + if l.length > 0 + group_user = g.ensure_group_user_exist() + g.transfer_memberships_to_group_account(l, group_user) + end + end + end + end +end diff --git a/app/models/group.rb b/app/models/group.rb index c22004c..5db028b 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -108,7 +108,7 @@ def for_each_admin def mail_admins_about_members(memberlist) l = memberlist.map { |e| e[:archived_at] = e[:archived_at].strftime("%B %d, %Y") - e[:balance] = %(#{e[:balance]} #{currency_symbol}) + e[:balance] = Money.new(e[:balance] * 100, currency_code).format } for_each_admin do |admin| UserMailer.notify_admins_archived_member_funds(admin: admin.member, group: self, memberlist: memberlist).deliver_later diff --git a/lib/tasks/cobudget.rake b/lib/tasks/cobudget.rake index cb6a81f..fad214d 100644 --- a/lib/tasks/cobudget.rake +++ b/lib/tasks/cobudget.rake @@ -5,5 +5,6 @@ namespace :cobudget do DeliverRecentActivityEmails.to_daily_digest_subscribers! DeliverRecentActivityEmails.to_weekly_digest_subscribers! DeliverCheckTransactionsEmail.check_transactions! + CobudgetCleanup.archived_members_with_funds! end end From 450a3866b8386274cb4087a9ed4b8bd6b3465c4d Mon Sep 17 00:00:00 2001 From: Michael Arnoldus Date: Mon, 27 Nov 2017 00:42:14 +0100 Subject: [PATCH 4/4] Added a config options so a devops user can be notified in case of archived members having funds --- app/mailers/user_mailer.rb | 10 ++++++---- app/models/group.rb | 19 +++++++++++++------ config/environments/development.rb | 3 +++ config/environments/production.rb | 3 +++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 6cdb21e..7fc5b6f 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -99,14 +99,16 @@ def notify_admins_archived_member_funds(admin: , group: ,memberlist: ) @memberlist = memberlist @group = group @group_user = group.ensure_group_user_exist() - mail(to: admin.name_and_email, + mail(to: admin, from: "Cobudget Updates ", subject: "Funds from archived members is returned to group account") end def check_transactions_email - mail(to: "devops@greaterthan.finance", - from: "Cobudget Updates ", - subject: "DB transactions consistency check") + if Rails.configuration.respond_to?('devops_user') + mail(to: Rails.configuration.devops_user, + from: "Cobudget Updates ", + subject: "DB transactions consistency check") + end end end diff --git a/app/models/group.rb b/app/models/group.rb index 5db028b..ce03db6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -106,12 +106,19 @@ def for_each_admin end def mail_admins_about_members(memberlist) - l = memberlist.map { |e| - e[:archived_at] = e[:archived_at].strftime("%B %d, %Y") - e[:balance] = Money.new(e[:balance] * 100, currency_code).format - } - for_each_admin do |admin| - UserMailer.notify_admins_archived_member_funds(admin: admin.member, group: self, memberlist: memberlist).deliver_later + if memberlist.length > 0 + l = memberlist.map { |e| + e[:archived_at] = e[:archived_at].strftime("%B %d, %Y") + e[:balance] = Money.new(e[:balance] * 100, currency_code).format + } + for_each_admin do |admin| + UserMailer.notify_admins_archived_member_funds(admin: admin.member.name_and_email, + group: self, memberlist: memberlist).deliver_later + end + if Rails.configuration.respond_to?('devops_user') + UserMailer.notify_admins_archived_member_funds(admin: Rails.configuration.devops_user, + group: self, memberlist: memberlist).deliver_later + end end end diff --git a/config/environments/development.rb b/config/environments/development.rb index abc4f21..c681cee 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -36,4 +36,7 @@ config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 } config.action_mailer.default_url_options = { host: 'localhost:9000' } config.action_mailer.raise_delivery_errors = true + + # Devops user. Mails will be sent to the user when it's sent to group admins + config.devops_user = "devops@yourdomain.org" end diff --git a/config/environments/production.rb b/config/environments/production.rb index 72c05bb..823107b 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -92,6 +92,9 @@ :password => ENV['SMTP_PASSWORD'], :domain => ENV['SMTP_DOMAIN'], :enable_starttls_auto => true + + config.devops_user = ENV['DEVOPS_MAIL'] + } end