Set member status to sustainer upon payment
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

Introduces a state machine for the payment status as well.

refs #213
This commit is contained in:
Râu Cao 2025-05-27 16:39:03 +04:00
parent 463bf34cdf
commit e48132cf5f
Signed by: raucao
GPG Key ID: 37036C356E56CC51
10 changed files with 73 additions and 24 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,11 +22,11 @@
</div>
</section>
<% if @donations_pending.any? %>
<% if @donations_processing.any? %>
<section class="donation-list">
<h2>Pending</h2>
<%= render partial: "contributions/donations/list",
locals: { donations: @donations_pending } %>
locals: { donations: @donations_processing } %>
</section>
<% end %>

View File

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

View File

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

View File

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

View File

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