diff --git a/Gemfile b/Gemfile index d691dee..20d1473 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'devise_ldap_authenticatable' gem 'net-ldap' # Utilities +gem 'aasm' gem "image_processing", "~> 1.12.2" gem "rqrcode", "~> 2.0" gem 'rails-settings-cached', '~> 2.8.3' diff --git a/Gemfile.lock b/Gemfile.lock index 396c65e..c96c0c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,8 @@ GEM remote: https://rubygems.org/ specs: + aasm (5.5.0) + concurrent-ruby (~> 1.0) actioncable (8.0.2) actionpack (= 8.0.2) activesupport (= 8.0.2) @@ -526,6 +528,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + aasm aws-sdk-s3 bcrypt (~> 3.1) capybara diff --git a/app/controllers/contributions/donations_controller.rb b/app/controllers/contributions/donations_controller.rb index b9f46c4..99e614e 100644 --- a/app/controllers/contributions/donations_controller.rb +++ b/app/controllers/contributions/donations_controller.rb @@ -11,7 +11,7 @@ class Contributions::DonationsController < ApplicationController def index @current_section = :contributions @donations_completed = current_user.donations.completed.order('paid_at desc') - @donations_pending = current_user.donations.processing.order('created_at desc') + @donations_processing = current_user.donations.processing.order('created_at desc') if Setting.lndhub_enabled? begin @@ -81,14 +81,11 @@ class Contributions::DonationsController < ApplicationController case invoice["status"] when "Settled" - @donation.paid_at = DateTime.now - @donation.payment_status = "settled" - @donation.save! + @donation.complete! flash_message = { success: "Thank you!" } when "Processing" unless @donation.processing? - @donation.payment_status = "processing" - @donation.save! + @donation.start_processing! flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." } BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation) end diff --git a/app/jobs/btcpay_check_donation_job.rb b/app/jobs/btcpay_check_donation_job.rb index 3cfdbcb..839dda8 100644 --- a/app/jobs/btcpay_check_donation_job.rb +++ b/app/jobs/btcpay_check_donation_job.rb @@ -10,9 +10,7 @@ class BtcpayCheckDonationJob < ApplicationJob case invoice["status"] when "Settled" - donation.paid_at = DateTime.now - donation.payment_status = "settled" - donation.save! + donation.complete! NotificationMailer.with(user: donation.user) .bitcoin_donation_confirmed diff --git a/app/models/donation.rb b/app/models/donation.rb index 9799ae6..b319259 100644 --- a/app/models/donation.rb +++ b/app/models/donation.rb @@ -1,22 +1,41 @@ class Donation < ApplicationRecord - # Relations + include AASM + belongs_to :user - # Validations validates_presence_of :user validates_presence_of :donation_method, inclusion: { in: %w[ custom btcpay lndhub ] } validates_presence_of :payment_status, allow_nil: true, - inclusion: { in: %w[ processing settled ] } + inclusion: { in: %w[ pending processing settled ] } validates_presence_of :paid_at, allow_nil: true validates_presence_of :amount_sats, allow_nil: true validates_presence_of :fiat_amount, allow_nil: true validates_presence_of :fiat_currency, allow_nil: true, inclusion: { in: %w[ EUR USD ] } - #Scopes + scope :pending, -> { where(payment_status: "pending") } scope :processing, -> { where(payment_status: "processing") } - scope :completed, -> { where(payment_status: "settled") } + scope :completed, -> { where(payment_status: "settled") } + + aasm column: :payment_status do + state :pending, initial: true + state :processing + state :settled + + event :start_processing do + transitions from: :pending, to: :processing + end + + event :complete do + transitions from: :processing, to: :settled, after: [:set_paid_at, :set_sustainer_status] + transitions from: :pending, to: :settled, after: [:set_paid_at, :set_sustainer_status] + end + end + + def pending? + payment_status == "pending" + end def processing? payment_status == "processing" @@ -25,4 +44,17 @@ class Donation < ApplicationRecord def completed? payment_status == "settled" end + + private + + def set_paid_at + update paid_at: DateTime.now if paid_at.nil? + end + + def set_sustainer_status + user.add_member_status :sustainer + rescue => e + Sentry.capture_exception(e) if Setting.sentry_enabled? + Rails.logger.error("Failed to set memberStatus: #{e.message}") + end end diff --git a/app/views/contributions/donations/index.html.erb b/app/views/contributions/donations/index.html.erb index 3e03617..5c9d6db 100644 --- a/app/views/contributions/donations/index.html.erb +++ b/app/views/contributions/donations/index.html.erb @@ -22,11 +22,11 @@ - <% if @donations_pending.any? %> + <% if @donations_processing.any? %>

Pending

<%= render partial: "contributions/donations/list", - locals: { donations: @donations_pending } %> + locals: { donations: @donations_processing } %>
<% end %> diff --git a/db/migrate/20250527113805_update_payment_status_to_pending.rb b/db/migrate/20250527113805_update_payment_status_to_pending.rb new file mode 100644 index 0000000..f334c21 --- /dev/null +++ b/db/migrate/20250527113805_update_payment_status_to_pending.rb @@ -0,0 +1,6 @@ +class UpdatePaymentStatusToPending < ActiveRecord::Migration[8.0] + def change + Donation.where(payment_status: nil).update_all(payment_status: "pending") + Donation.where.not(payment_status: %w[pending processing settled]).update_all(payment_status: "pending") + end +end diff --git a/db/schema.rb b/db/schema.rb index 1140431..b7746ac 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_05_17_105755) do +ActiveRecord::Schema[8.0].define(version: 2025_05_27_113805) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false diff --git a/spec/jobs/btcpay_check_donation_job_spec.rb b/spec/jobs/btcpay_check_donation_job_spec.rb index 71cd1df..6e12adb 100644 --- a/spec/jobs/btcpay_check_donation_job_spec.rb +++ b/spec/jobs/btcpay_check_donation_job_spec.rb @@ -8,16 +8,18 @@ RSpec.describe BtcpayCheckDonationJob, type: :job do user.donations.create!( donation_method: "btcpay", btcpay_invoice_id: "K4e31MhbLKmr3D7qoNYRd3", - paid_at: nil, payment_status: "processing", - fiat_amount: 120, fiat_currency: "USD" + paid_at: nil, + payment_status: "processing", + fiat_amount: 120, + fiat_currency: "USD" ) end before do allow_any_instance_of(User).to receive(:ldap_entry).and_return({ - uid: user.cn, ou: user.ou, mail: user.email, admin: nil, - display_name: nil + uid: user.cn, ou: user.ou, mail: user.email, admin: nil, display_name: nil }) + allow_any_instance_of(User).to receive(:add_member_status) end after(:each) do @@ -65,15 +67,20 @@ RSpec.describe BtcpayCheckDonationJob, type: :job do it "notifies the user via email" do perform_enqueued_jobs(only: described_class) { job } expect(enqueued_jobs.size).to eq(1) - job = enqueued_jobs.select{|j| j['job_class'] == "ActionMailer::MailDeliveryJob"}.first + job = enqueued_jobs.select { |j| j['job_class'] == "ActionMailer::MailDeliveryJob" }.first expect(job['arguments'][0]).to eq('NotificationMailer') expect(job['arguments'][1]).to eq('bitcoin_donation_confirmed') - expect(job['arguments'][3]['params']['user']['_aj_globalid']).to eq('gid://akkounts/User/1') + expect(job['arguments'][3]['params']['user']['_aj_globalid']).to eq(user.to_global_id.to_s) end it "does not enqueue itself again" do expect_any_instance_of(described_class).not_to receive(:re_enqueue_job) perform_enqueued_jobs(only: described_class) { job } end + + it "updates the user's member status" do + expect_any_instance_of(User).to receive(:add_member_status).with(:sustainer) + perform_enqueued_jobs(only: described_class) { job } + end end end diff --git a/spec/requests/contributions/donations_spec.rb b/spec/requests/contributions/donations_spec.rb index 01397eb..e6d1392 100644 --- a/spec/requests/contributions/donations_spec.rb +++ b/spec/requests/contributions/donations_spec.rb @@ -177,7 +177,7 @@ RSpec.describe "Donations", type: :request do .to_return(status: 200, headers: {}, body: invoice) stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/MCkDbf2cUgBuuisUCgnRnb/payment-methods") .to_return(status: 200, headers: {}, body: payments) - + allow(user).to receive(:add_member_status).with(:sustainer).and_return(["sustainer"]) get confirm_btcpay_contributions_donation_path(subject) end @@ -185,11 +185,16 @@ RSpec.describe "Donations", type: :request do subject.reload expect(subject.paid_at).not_to be_nil expect(subject.amount_sats).to eq(2061) + expect(subject.payment_status).to eq("settled") end it "redirects to the donations index" do expect(response).to redirect_to(contributions_donations_url) end + + it "updates the user's member status" do + expect(user).to have_received(:add_member_status).with(:sustainer) + end end describe "amount in sats" do