Let users donate sats via BTCPay Server #176
@ -31,6 +31,7 @@
|
||||
# Service Integrations
|
||||
#
|
||||
|
||||
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||
# BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||
# BTCPAY_STORE_ID=''
|
||||
# BTCPAY_AUTH_TOKEN=''
|
||||
|
@ -2,6 +2,7 @@ PRIMARY_DOMAIN=kosmos.org
|
||||
|
||||
REDIS_URL='redis://localhost:6379/0'
|
||||
|
||||
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||
BTCPAY_STORE_ID='123456'
|
||||
|
||||
|
@ -32,6 +32,11 @@
|
||||
focus:ring-blue-400 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn-emerald {
|
||||
@apply bg-emerald-500 hover:bg-emerald-600 text-white
|
||||
focus:ring-emerald-400 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn-red {
|
||||
@apply bg-red-600 hover:bg-red-700 text-white
|
||||
focus:ring-red-500 focus:ring-opacity-75;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="md:min-h-[50vh] bg-white rounded-lg shadow">
|
||||
<div class="px-6 sm:px-12 pt-2 sm:pt-4">
|
||||
<%= render partial: @tabnav_partial %>
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
<!-- Modal Container -->
|
||||
<div data-modal-target="container"
|
||||
class="max-h-screen w-auto max-w-lg relative
|
||||
class="relative m-4 max-h-screen w-auto max-w-full
|
||||
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
|
||||
<!-- Modal Card -->
|
||||
<div class="m-1 bg-white rounded shadow">
|
||||
|
@ -34,6 +34,8 @@ class NotificationComponent < ViewComponent::Base
|
||||
'alert-octagon'
|
||||
when 'alert'
|
||||
'alert-octagon'
|
||||
when 'warning'
|
||||
'alert-octagon'
|
||||
else
|
||||
'info'
|
||||
end
|
||||
|
@ -3,18 +3,16 @@ class Admin::DonationsController < Admin::BaseController
|
||||
before_action :set_current_section, only: [:index, :show, :new, :edit]
|
||||
|
||||
# GET /donations
|
||||
# GET /donations.json
|
||||
def index
|
||||
@pagy, @donations = pagy(Donation.all.order('created_at desc'))
|
||||
@pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
|
||||
|
||||
@stats = {
|
||||
overall_sats: @donations.all.sum("amount_sats"),
|
||||
donor_count: Donation.distinct.count(:user_id)
|
||||
overall_sats: @donations.sum("amount_sats"),
|
||||
donor_count: @donations.distinct.count(:user_id)
|
||||
}
|
||||
end
|
||||
|
||||
# GET /donations/1
|
||||
# GET /donations/1.json
|
||||
def show
|
||||
end
|
||||
|
||||
@ -28,55 +26,42 @@ class Admin::DonationsController < Admin::BaseController
|
||||
end
|
||||
|
||||
# POST /donations
|
||||
# POST /donations.json
|
||||
def create
|
||||
@donation = Donation.new(donation_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @donation.paid_at == nil
|
||||
@donation.errors.add(:paid_at, message: "is required")
|
||||
render :new, status: :unprocessable_entity and return
|
||||
end
|
||||
|
||||
if @donation.save
|
||||
format.html do
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully created.'
|
||||
}
|
||||
end
|
||||
format.json { render :show, status: :created, location: @donation }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
||||
end
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /donations/1
|
||||
# PATCH/PUT /donations/1.json
|
||||
# PUT /donations/1
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @donation.update(donation_params)
|
||||
format.html do
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully updated.'
|
||||
}
|
||||
end
|
||||
format.json { render :show, status: :ok, location: @donation }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
||||
end
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /donations/1
|
||||
# DELETE /donations/1.json
|
||||
def destroy
|
||||
@donation.destroy
|
||||
respond_to do |format|
|
||||
format.html do redirect_to admin_donations_url, flash: {
|
||||
|
||||
redirect_to admin_donations_url, flash: {
|
||||
success: 'Donation was successfully destroyed.'
|
||||
}
|
||||
end
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
@ -86,7 +71,10 @@ class Admin::DonationsController < Admin::BaseController
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def donation_params
|
||||
params.require(:donation).permit(:user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :paid_at)
|
||||
params.require(:donation).permit(
|
||||
:user_id, :donation_method,
|
||||
:amount_sats, :fiat_amount, :fiat_currency,
|
||||
:public_name, :paid_at)
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
|
@ -41,4 +41,26 @@ class ApplicationController < ActionController::Base
|
||||
def after_sign_in_path_for(user)
|
||||
session[:user_return_to] || root_path
|
||||
end
|
||||
|
||||
def lndhub_authenticate(options={})
|
||||
if session[:ln_auth_token].present? && !options[:force_reauth]
|
||||
@ln_auth_token = session[:ln_auth_token]
|
||||
else
|
||||
lndhub = Lndhub.new
|
||||
auth_token = lndhub.authenticate(current_user)
|
||||
session[:ln_auth_token] = auth_token
|
||||
@ln_auth_token = auth_token
|
||||
end
|
||||
rescue => e
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
|
||||
def lndhub_fetch_balance
|
||||
@balance = LndhubManager::FetchUserBalance.call(auth_token: @ln_auth_token)
|
||||
rescue AuthError
|
||||
lndhub_authenticate(force_reauth: true)
|
||||
raise if @fetch_balance_retried
|
||||
@fetch_balance_retried = true
|
||||
lndhub_fetch_balance
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,129 @@
|
||||
class Contributions::DonationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
include BtcpayHelper
|
||||
|
||||
# GET /donations
|
||||
# GET /donations.json
|
||||
before_action :authenticate_user!
|
||||
before_action :set_donation_methods, only: [:index, :create]
|
||||
before_action :require_donation_method_enabled, only: [:create]
|
||||
before_action :validate_donation_params, only: [:create]
|
||||
before_action :set_donation, only: [:confirm_btcpay]
|
||||
|
||||
# GET /contributions/donations
|
||||
def index
|
||||
@donations = current_user.donations.completed
|
||||
@current_section = :contributions
|
||||
@donations_completed = current_user.donations.completed.order('paid_at desc')
|
||||
@donations_pending = current_user.donations.processing.order('created_at desc')
|
||||
|
||||
if Setting.lndhub_enabled?
|
||||
begin
|
||||
lndhub_authenticate
|
||||
lndhub_fetch_balance
|
||||
rescue
|
||||
@balance = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# POST /contributions/donations
|
||||
def create
|
||||
if params[:currency] == "sats"
|
||||
fiat_amount = nil
|
||||
fiat_currency = nil
|
||||
raucao marked this conversation as resolved
|
||||
amount_sats = params[:amount]
|
||||
else
|
||||
fiat_amount = params[:amount].to_i
|
||||
fiat_currency = params[:currency]
|
||||
amount_sats = nil
|
||||
end
|
||||
|
||||
@donation = current_user.donations.create!(
|
||||
donation_method: params[:donation_method],
|
||||
payment_method: nil,
|
||||
paid_at: nil,
|
||||
amount_sats: amount_sats,
|
||||
fiat_amount: (fiat_amount.nil? ? nil : fiat_amount * 100), # store in cents
|
||||
fiat_currency: fiat_currency,
|
||||
public_name: params[:public_name]
|
||||
)
|
||||
|
||||
case params[:donation_method]
|
||||
when "btcpay"
|
||||
res = BtcpayManager::CreateInvoice.call(
|
||||
amount: fiat_amount || (amount_sats.to_f / 100000000),
|
||||
currency: fiat_currency || "BTC",
|
||||
redirect_url: confirm_btcpay_contributions_donation_url(@donation)
|
||||
)
|
||||
|
||||
@donation.update! btcpay_invoice_id: res["id"]
|
||||
|
||||
redirect_to btcpay_checkout_url(res["id"]), allow_other_host: true
|
||||
else
|
||||
redirect_to contributions_donations_url, flash: {
|
||||
error: "Donation method currently not available"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_btcpay
|
||||
redirect_to contributions_donations_url and return if @donation.completed?
|
||||
|
||||
invoice = BtcpayManager::FetchInvoice.call(invoice_id: @donation.btcpay_invoice_id)
|
||||
|
||||
if @donation.amount_sats.present?
|
||||
# TODO make default fiat currency configurable and/or determine from user's
|
||||
# i18n browser settings
|
||||
@donation.fiat_currency = "EUR"
|
||||
exchange_rate = BtcpayManager::FetchExchangeRate.call(fiat_currency: @donation.fiat_currency)
|
||||
@donation.fiat_amount = (((@donation.amount_sats.to_f / 100000000) * exchange_rate) * 100).to_i
|
||||
else
|
||||
amt_str = invoice["paymentMethods"].first["amount"]
|
||||
@donation.amount_sats = amt_str.tr(".","").sub(/0*$/, "").to_i
|
||||
end
|
||||
|
||||
case invoice["status"]
|
||||
when "Settled"
|
||||
@donation.paid_at = DateTime.now
|
||||
@donation.payment_status = "settled"
|
||||
@donation.save!
|
||||
flash_message = { success: "Thank you!" }
|
||||
when "Processing"
|
||||
unless @donation.processing?
|
||||
@donation.payment_status = "processing"
|
||||
@donation.save!
|
||||
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
|
||||
when "Expired"
|
||||
flash_message = { warning: "The payment request for this donation has expired" }
|
||||
else
|
||||
flash_message = { warning: "Could not determine status of payment" }
|
||||
end
|
||||
|
||||
redirect_to contributions_donations_url, flash: flash_message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_donation
|
||||
@donation = current_user.donations.find_by(id: params[:id])
|
||||
http_status :not_found unless @donation.present?
|
||||
end
|
||||
|
||||
def set_donation_methods
|
||||
@donation_methods = []
|
||||
@donation_methods.push :btcpay if Setting.btcpay_enabled?
|
||||
@donation_methods.push :lndhub if Setting.lndhub_enabled?
|
||||
@donation_methods.push :opencollective if Setting.opencollective_enabled?
|
||||
end
|
||||
|
||||
def require_donation_method_enabled
|
||||
http_status :forbidden unless @donation_methods.include?(
|
||||
params[:donation_method].to_sym
|
||||
)
|
||||
end
|
||||
|
||||
def validate_donation_params
|
||||
if !%w[EUR USD sats].include?(params[:currency]) || (params[:amount].to_i <= 0)
|
||||
http_status :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2,10 +2,11 @@ require "rqrcode"
|
||||
require "lnurl"
|
||||
|
||||
class Services::LightningController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :authenticate_with_lndhub
|
||||
before_action :set_current_section
|
||||
before_action :fetch_balance
|
||||
before_action :require_service_available
|
||||
before_action :authenticate_user!
|
||||
before_action :lndhub_authenticate
|
||||
before_action :lndhub_fetch_balance
|
||||
|
||||
def index
|
||||
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
||||
@ -55,32 +56,12 @@ class Services::LightningController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def authenticate_with_lndhub(options={})
|
||||
if session[:ln_auth_token].present? && !options[:force_reauth]
|
||||
@ln_auth_token = session[:ln_auth_token]
|
||||
else
|
||||
lndhub = Lndhub.new
|
||||
auth_token = lndhub.authenticate(current_user)
|
||||
session[:ln_auth_token] = auth_token
|
||||
@ln_auth_token = auth_token
|
||||
end
|
||||
rescue => e
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
@current_section = :services
|
||||
end
|
||||
|
||||
def fetch_balance
|
||||
lndhub = Lndhub.new
|
||||
data = lndhub.balance @ln_auth_token
|
||||
@balance = data["BTC"]["AvailableBalance"] rescue nil
|
||||
rescue AuthError
|
||||
authenticate_with_lndhub(force_reauth: true)
|
||||
raise if @fetch_balance_retried
|
||||
@fetch_balance_retried = true
|
||||
fetch_balance
|
||||
def require_service_available
|
||||
http_status :not_found unless Setting.lndhub_enabled?
|
||||
end
|
||||
|
||||
def fetch_transactions
|
||||
|
@ -1,10 +1,6 @@
|
||||
module ApplicationHelper
|
||||
include Pagy::Frontend
|
||||
|
||||
def sats_to_btc(sats)
|
||||
sats.to_f / 100000000
|
||||
end
|
||||
|
||||
def main_nav_class(current_section, link_to_section)
|
||||
if current_section == link_to_section
|
||||
"bg-gray-900/50 text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
|
||||
|
7
app/helpers/btcpay_helper.rb
Normal file
7
app/helpers/btcpay_helper.rb
Normal file
@ -0,0 +1,7 @@
|
||||
module BtcpayHelper
|
||||
|
||||
def btcpay_checkout_url(invoice_id)
|
||||
"#{Setting.btcpay_public_url}/i/#{invoice_id}"
|
||||
end
|
||||
|
||||
end
|
28
app/jobs/btcpay_check_donation_job.rb
Normal file
28
app/jobs/btcpay_check_donation_job.rb
Normal file
@ -0,0 +1,28 @@
|
||||
class BtcpayCheckDonationJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(donation)
|
||||
return if donation.completed?
|
||||
|
||||
invoice = BtcpayManager::FetchInvoice.call(
|
||||
invoice_id: donation.btcpay_invoice_id
|
||||
)
|
||||
|
||||
case invoice["status"]
|
||||
when "Settled"
|
||||
donation.paid_at = DateTime.now
|
||||
donation.payment_status = "settled"
|
||||
donation.save!
|
||||
|
||||
NotificationMailer.with(user: donation.user)
|
||||
.bitcoin_donation_confirmed
|
||||
.deliver_later
|
||||
when "Processing"
|
||||
re_enqueue_job(donation)
|
||||
end
|
||||
end
|
||||
|
||||
def re_enqueue_job(donation)
|
||||
self.class.set(wait: 20.seconds).perform_later(donation)
|
||||
end
|
||||
end
|
@ -23,4 +23,11 @@ class NotificationMailer < ApplicationMailer
|
||||
@subject = "New invitations added to your account"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
|
||||
def bitcoin_donation_confirmed
|
||||
@user = params[:user]
|
||||
@donation = params[:donation]
|
||||
@subject = "Donation confirmed"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
end
|
||||
|
@ -4,12 +4,25 @@ class Donation < ApplicationRecord
|
||||
|
||||
# Validations
|
||||
validates_presence_of :user
|
||||
validates_presence_of :amount_sats
|
||||
validates_presence_of :paid_at
|
||||
|
||||
# Hooks
|
||||
# TODO before_create :store_fiat_value
|
||||
validates_presence_of :donation_method,
|
||||
inclusion: { in: %w[ custom btcpay lndhub ] }
|
||||
validates_presence_of :payment_status, allow_nil: true,
|
||||
inclusion: { in: %w[ 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 :completed, -> { where.not(paid_at: nil) }
|
||||
scope :processing, -> { where(payment_status: "processing") }
|
||||
scope :completed, -> { where(payment_status: "settled") }
|
||||
|
||||
def processing?
|
||||
payment_status == "processing"
|
||||
end
|
||||
|
||||
def completed?
|
||||
payment_status == "settled"
|
||||
end
|
||||
end
|
||||
|
@ -51,6 +51,9 @@ class Setting < RailsSettings::Base
|
||||
field :btcpay_enabled, type: :boolean,
|
||||
default: ENV["BTCPAY_API_URL"].present?
|
||||
|
||||
field :btcpay_public_url, type: :string,
|
||||
default: ENV["BTCPAY_PUBLIC_URL"].presence
|
||||
|
||||
field :btcpay_store_id, type: :string,
|
||||
default: ENV["BTCPAY_STORE_ID"].presence
|
||||
|
||||
@ -157,7 +160,13 @@ class Setting < RailsSettings::Base
|
||||
# Nostr
|
||||
#
|
||||
|
||||
field :nostr_enabled, type: :boolean, default: true
|
||||
field :nostr_enabled, type: :boolean, default: false
|
||||
|
||||
#
|
||||
# OpenCollective
|
||||
#
|
||||
|
||||
field :opencollective_enabled, type: :boolean, default: true
|
||||
|
||||
#
|
||||
# RemoteStorage
|
||||
|
21
app/services/btcpay_manager/create_invoice.rb
Normal file
21
app/services/btcpay_manager/create_invoice.rb
Normal file
@ -0,0 +1,21 @@
|
||||
module BtcpayManager
|
||||
class CreateInvoice < BtcpayManagerService
|
||||
def initialize(amount:, currency:, redirect_url:)
|
||||
@amount = amount
|
||||
@currency = currency
|
||||
@redirect_url = redirect_url
|
||||
end
|
||||
|
||||
def call
|
||||
post "/invoices", {
|
||||
amount: @amount.to_s,
|
||||
currency: @currency,
|
||||
checkout: {
|
||||
redirectURL: @redirect_url,
|
||||
redirectAutomatically: true,
|
||||
requiresRefundEmail: false
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
14
app/services/btcpay_manager/fetch_exchange_rate.rb
Normal file
14
app/services/btcpay_manager/fetch_exchange_rate.rb
Normal file
@ -0,0 +1,14 @@
|
||||
module BtcpayManager
|
||||
class FetchExchangeRate < BtcpayManagerService
|
||||
def initialize(fiat_currency:)
|
||||
@fiat_currency = fiat_currency
|
||||
end
|
||||
|
||||
def call
|
||||
pair_str = "BTC_#{@fiat_currency}"
|
||||
res = get "rates", { currencyPair: pair_str }
|
||||
pair = res.find{|p| p["currencyPair"] == pair_str }
|
||||
rate = pair["rate"].to_f
|
||||
end
|
||||
end
|
||||
end
|
14
app/services/btcpay_manager/fetch_invoice.rb
Normal file
14
app/services/btcpay_manager/fetch_invoice.rb
Normal file
@ -0,0 +1,14 @@
|
||||
module BtcpayManager
|
||||
class FetchInvoice < BtcpayManagerService
|
||||
def initialize(invoice_id:)
|
||||
@invoice_id = invoice_id
|
||||
end
|
||||
|
||||
def call
|
||||
invoice = get "/invoices/#{@invoice_id}"
|
||||
payment_methods = get "/invoices/#{@invoice_id}/payment-methods"
|
||||
invoice["paymentMethods"] = payment_methods
|
||||
invoice
|
||||
end
|
||||
end
|
||||
end
|
@ -1,7 +1,7 @@
|
||||
module BtcpayManager
|
||||
class FetchLightningWalletBalance < BtcpayManagerService
|
||||
def call
|
||||
res = get "stores/#{store_id}/lightning/BTC/balance"
|
||||
res = get "/lightning/BTC/balance"
|
||||
|
||||
{
|
||||
confirmed_balance: res["offchain"]["local"].to_i / 1000 # msats to sats
|
||||
|
@ -1,7 +1,7 @@
|
||||
module BtcpayManager
|
||||
class FetchOnchainWalletBalance < BtcpayManagerService
|
||||
def call
|
||||
res = get "stores/#{store_id}/payment-methods/onchain/BTC/wallet"
|
||||
res = get "/payment-methods/onchain/BTC/wallet"
|
||||
|
||||
{
|
||||
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
|
||||
|
@ -2,23 +2,35 @@
|
||||
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
|
||||
#
|
||||
class BtcpayManagerService < ApplicationService
|
||||
attr_reader :base_url, :store_id, :auth_token
|
||||
|
||||
def initialize
|
||||
@base_url = Setting.btcpay_api_url
|
||||
@store_id = Setting.btcpay_store_id
|
||||
@auth_token = Setting.btcpay_auth_token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get(endpoint)
|
||||
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
|
||||
def base_url
|
||||
@base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
|
||||
end
|
||||
|
||||
def auth_token
|
||||
@auth_token ||= Setting.btcpay_auth_token
|
||||
end
|
||||
|
||||
def headers
|
||||
{
|
||||
"Content-Type" => "application/json",
|
||||
"Accept" => "application/json",
|
||||
"Authorization" => "token #{auth_token}"
|
||||
})
|
||||
}
|
||||
end
|
||||
|
||||
def endpoint_url(path)
|
||||
"#{base_url}/#{path.gsub(/^\//, '')}"
|
||||
end
|
||||
|
||||
def get(path, params = {})
|
||||
res = Faraday.get endpoint_url(path), params, headers
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
|
||||
def post(path, payload)
|
||||
res = Faraday.post endpoint_url(path), payload.to_json, headers
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
end
|
||||
|
@ -1,24 +1,20 @@
|
||||
class Lndhub
|
||||
class Lndhub < ApplicationService
|
||||
attr_accessor :auth_token
|
||||
|
||||
def initialize
|
||||
@base_url = ENV["LNDHUB_API_URL"]
|
||||
end
|
||||
|
||||
def post(endpoint, payload)
|
||||
def post(path, payload)
|
||||
headers = { "Content-Type" => "application/json" }
|
||||
if auth_token
|
||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||
end
|
||||
|
||||
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
||||
res = Faraday.post endpoint_url(path), payload.to_json, headers
|
||||
log_error(res) if res.status != 200
|
||||
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
|
||||
def get(endpoint, auth_token)
|
||||
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
|
||||
def get(path, auth_token)
|
||||
res = Faraday.get(endpoint_url(path), {}, {
|
||||
"Content-Type" => "application/json",
|
||||
"Accept" => "application/json",
|
||||
"Authorization" => "Bearer #{auth_token}"
|
||||
@ -42,7 +38,7 @@ class Lndhub
|
||||
self.auth_token
|
||||
end
|
||||
|
||||
def balance(user_token=nil)
|
||||
def fetch_balance(user_token=nil)
|
||||
get "balance", user_token || auth_token
|
||||
end
|
||||
|
||||
@ -72,4 +68,14 @@ class Lndhub
|
||||
Sentry.capture_message("Lndhub API request failed: #{res.body}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_url
|
||||
@base_url ||= Setting.lndhub_api_url
|
||||
end
|
||||
|
||||
def endpoint_url(path)
|
||||
"#{base_url}/#{path.gsub(/^\//, '')}"
|
||||
end
|
||||
end
|
||||
|
12
app/services/lndhub_manager/fetch_user_balance.rb
Normal file
12
app/services/lndhub_manager/fetch_user_balance.rb
Normal file
@ -0,0 +1,12 @@
|
||||
module LndhubManager
|
||||
class FetchUserBalance < Lndhub
|
||||
def initialize(auth_token:)
|
||||
@auth_token = auth_token
|
||||
end
|
||||
|
||||
def call
|
||||
data = fetch_balance(auth_token)
|
||||
data["BTC"]["AvailableBalance"] rescue nil
|
||||
end
|
||||
end
|
||||
end
|
@ -1,13 +1,13 @@
|
||||
class LndhubV2 < Lndhub
|
||||
|
||||
def post(endpoint, payload, options={})
|
||||
def post(path, payload, options={})
|
||||
headers = { "Content-Type" => "application/json" }
|
||||
if auth_token
|
||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||
elsif options[:admin_token]
|
||||
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
|
||||
end
|
||||
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
||||
res = Faraday.post endpoint_url(path), payload.to_json, headers
|
||||
log_error(res) if res.status != 200
|
||||
|
||||
JSON.parse(res.body)
|
||||
|
@ -1,2 +0,0 @@
|
||||
json.extract! donation, :id, :user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :created_at, :updated_at
|
||||
json.url donation_url(donation, format: :json)
|
@ -14,14 +14,24 @@
|
||||
<%= form.label :user_id %>
|
||||
<%= form.collection_select :user_id, User.where(ou: Setting.primary_domain).order(:cn), :id, :cn, {} %>
|
||||
|
||||
<%= form.label :donation_method, "Donation method" %>
|
||||
<%= form.select :donation_method, options_for_select([
|
||||
["Custom (manual)", "custom"],
|
||||
["BTCPay", "btcpay"],
|
||||
["LndHub account", "lndhub"],
|
||||
["OpenCollective", "opencollective"]
|
||||
], selected: (donation.donation_method || "custom")) %>
|
||||
|
||||
<%= form.label :amount_sats, "Amount BTC (sats)" %>
|
||||
<%= form.number_field :amount_sats %>
|
||||
|
||||
<%= form.label :amount_eur, "Amount EUR (cents)" %>
|
||||
<%= form.number_field :amount_eur %>
|
||||
<%= form.label :fiat_amount, "Fiat Amount (cents)" %>
|
||||
<%= form.number_field :fiat_amount %>
|
||||
|
||||
<%= form.label :amount_usd, "Amount USD (cents)"%>
|
||||
<%= form.number_field :amount_usd %>
|
||||
<%= form.label :fiat_currency, "Fiat Currency" %>
|
||||
<%= form.select :fiat_currency, options_for_select([
|
||||
["EUR", "EUR"], ["USD", "USD"], ["sats", "sats"]
|
||||
], selected: donation.fiat_currency) %>
|
||||
|
||||
<%= form.label :public_name %>
|
||||
<%= form.text_field :public_name %>
|
||||
|
@ -25,9 +25,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th class="text-right">Amount BTC</th>
|
||||
<th class="text-right">in EUR</th>
|
||||
<th class="text-right">in USD</th>
|
||||
<th class="text-right">Sats</th>
|
||||
<th class="text-right">Fiat Amount</th>
|
||||
<th class="pl-2">Public name</th>
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
@ -37,9 +36,8 @@
|
||||
<% @donations.each do |donation| %>
|
||||
<tr>
|
||||
<td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
|
||||
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td>
|
||||
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
|
||||
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
|
||||
<td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></td>
|
||||
<td class="text-right"><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td>
|
||||
<td class="pl-2"><%= donation.public_name %></td>
|
||||
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
|
||||
<td class="text-right">
|
||||
|
@ -1 +0,0 @@
|
||||
json.array! @donations, partial: "donations/donation", as: :donation
|
@ -8,17 +8,17 @@
|
||||
<th>User</th>
|
||||
<td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Donation Method</th>
|
||||
<td><%= @donation.donation_method %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Amount sats</th>
|
||||
<td><%= @donation.amount_sats %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Amount EUR</th>
|
||||
<td><%= @donation.amount_eur %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Amount USD</th>
|
||||
<td><%= @donation.amount_usd %></td>
|
||||
<th>Fiat amount</th>
|
||||
<td><% if @donation.fiat_amount.present? %><%= number_to_currency @donation.fiat_amount.to_f / 100, unit: "" %> <%= @donation.fiat_currency %><% end %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Public name</th>
|
||||
@ -26,7 +26,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td><%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||
<td><%= @donation.paid_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -1 +0,0 @@
|
||||
json.partial! "donations/donation", donation: @donation
|
36
app/views/contributions/donations/_bitcoin.html.erb
Normal file
36
app/views/contributions/donations/_bitcoin.html.erb
Normal file
@ -0,0 +1,36 @@
|
||||
<div class="rounded-lg p-6 bg-emerald-50 hover:bg-emerald-100 transition-colors">
|
||||
<h3 class="mb-4 text-lg font-bold">Donate directly with Bitcoin</h3>
|
||||
<p class="mb-6">
|
||||
Open-source money for open-source services.
|
||||
</p>
|
||||
<div data-controller="modal" data-action="keydown.esc->modal#close">
|
||||
<button class="btn-md btn-emerald w-full lg:w-1/2" data-action="click->modal#open">
|
||||
Donate
|
||||
</button>
|
||||
<%= render ModalComponent.new(show_close_button: false) do %>
|
||||
<div>
|
||||
<h3>Your contribution</h3>
|
||||
|
||||
<%= form_with(url: contributions_donations_url, method: :post) do |f| %>
|
||||
<%= f.hidden_field :donation_method, value: "btcpay" %>
|
||||
|
||||
<div class="mb-6 flex gap-2">
|
||||
<%= f.number_field :amount, required: true %>
|
||||
<%= f.select :currency, options_for_select([
|
||||
["EUR", "EUR"], ["USD", "USD"], ["sats", "sats"]
|
||||
], selected: "EUR"), class: "flex-none" %>
|
||||
</div>
|
||||
|
||||
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Public name") do %>
|
||||
<%= f.text_field :public_name, class: "w-full", placeholder: "Anonymous" %>
|
||||
<% end %>
|
||||
|
||||
<p class="mt-12">
|
||||
<%= f.submit 'Continue', data: { turbo: false },
|
||||
class: "btn-md btn-blue w-full" %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
37
app/views/contributions/donations/_list.html.erb
Normal file
37
app/views/contributions/donations/_list.html.erb
Normal file
@ -0,0 +1,37 @@
|
||||
<ul class="list-none">
|
||||
<% donations.each do |donation| %>
|
||||
<li class="mb-8 grid gap-y-2 grid-cols-2 items-center">
|
||||
<h3 class="mb-0">
|
||||
<% if donation.completed? %>
|
||||
<%= donation.paid_at.strftime("%B %d, %Y") %>
|
||||
<% else %>
|
||||
<%= donation.created_at.strftime("%B %d, %Y") %>
|
||||
<% end %>
|
||||
</h3>
|
||||
<p class="row-span-2 font-mono text-right mb-0">
|
||||
<span class="text-xl">
|
||||
<%= number_with_delimiter donation.amount_sats %> sats
|
||||
</span>
|
||||
<br>
|
||||
<span class="text-sm text-gray-500">
|
||||
(~ <%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %>)
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-0 text-gray-500">
|
||||
<% if donation.processing? %>
|
||||
Waiting for confirmations
|
||||
<% if donation.donation_method == "btcpay" %>
|
||||
<%= link_to "check status", btcpay_checkout_url(donation.btcpay_invoice_id),
|
||||
class: "ml-2 btn-sm btn-gray" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% if donation.public_name.present? %>
|
||||
As: <%= donation.public_name %>
|
||||
<% else %>
|
||||
Anonymous
|
||||
<% end %>
|
||||
<% end %>
|
||||
</p>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
@ -0,0 +1,6 @@
|
||||
<div class="rounded-lg p-6 bg-zinc-100 hover:bg-zinc-200 transition-colors">
|
||||
<h3 class="mb-4 text-lg font-bold text-gray-500">Donate via OpenCollective</h3>
|
||||
<p class="text-gray-600 text-gray-500">
|
||||
Coming soon.
|
||||
</p>
|
||||
</div>
|
@ -2,50 +2,39 @@
|
||||
|
||||
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
|
||||
<section>
|
||||
<% if @donations.any? %>
|
||||
<p class="mb-12">
|
||||
Your financial contributions to the development and upkeep of Kosmos
|
||||
software and services.
|
||||
</p>
|
||||
<ul class="list-none">
|
||||
<% @donations.each do |donation| %>
|
||||
<li class="mb-8 grid gap-y-2 gap-x-8 grid-cols-2 items-center">
|
||||
<h3 class="mb-0">
|
||||
<%= donation.paid_at.strftime("%B %d, %Y") %>
|
||||
</h3>
|
||||
<p class="row-span-2 font-mono text-right mb-0">
|
||||
<span class="text-xl">
|
||||
<%= number_with_delimiter donation.amount_sats %> sats
|
||||
</span>
|
||||
<br>
|
||||
<span class="text-sm text-gray-500">
|
||||
(~ <%= number_to_currency donation.amount_eur / 100, unit: "" %> EUR)
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<% if donation.public_name.present? %>
|
||||
Public name: <%= donation.public_name %>
|
||||
<% else %>
|
||||
Anonymous
|
||||
</section>
|
||||
|
||||
<section class="donation-methods">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||
<% if @donation_methods.include?(:btcpay) ||
|
||||
@donation_methods.include?(:lndhub) %>
|
||||
<%= render partial: "contributions/donations/bitcoin", locals: {
|
||||
donation_methods: @donation_methods, lndhub_balance: @balance
|
||||
} %>
|
||||
<% end %>
|
||||
</p>
|
||||
</li>
|
||||
<% if @donation_methods.include?(:opencollective) %>
|
||||
<%= render partial: "contributions/donations/opencollective" %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<div class="text-center">
|
||||
<p class="mt-8 mb-12 inline-flex align-center items-center">
|
||||
<%= image_tag("/img/illustrations/undraw_savings_re_eq4w.svg", class: 'h-48') %>
|
||||
</p>
|
||||
<h3>
|
||||
No donations yet
|
||||
</h3>
|
||||
<p class="text-gray-500">
|
||||
The donation process is not automated yet.<br>Please
|
||||
<a href="https://wiki.kosmos.org/Main_Page#Community_.2F_Getting_in_touch_.2F_Getting_involved" class="ks-text-link" target="_blank">contact us</a>
|
||||
if you'd like to contribute this way right now.
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<% if @donations_pending.any? %>
|
||||
<section class="donation-list">
|
||||
<h2>Pending</h2>
|
||||
<%= render partial: "contributions/donations/list",
|
||||
locals: { donations: @donations_pending } %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<% if @donations_completed.any? %>
|
||||
<section class="donation-list">
|
||||
<h2>Past contributions</h2>
|
||||
<%= render partial: "contributions/donations/list",
|
||||
locals: { donations: @donations_completed } %>
|
||||
</section>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
@ -0,0 +1,11 @@
|
||||
Hi <%= @user.display_name.presence || @user.cn %>,
|
||||
|
||||
Your bitcoin donation has been confirmed successfully. <3
|
||||
|
||||
Thank you so much for helping us with keeping the lights on, as well as with continually improving our services for you!
|
||||
|
||||
You can find all of your past financial contributions on this page:
|
||||
|
||||
<%= contributions_donations_url %>
|
||||
|
||||
Have a nice day!
|
6
app/views/shared/status_unprocessable_entity.html.erb
Normal file
6
app/views/shared/status_unprocessable_entity.html.erb
Normal file
@ -0,0 +1,6 @@
|
||||
<%= render HeaderCompactComponent.new(title: "422") %>
|
||||
|
||||
<%= render MainCompactComponent.new do %>
|
||||
<h2>Unprocessable content</h2>
|
||||
<p>The data provided was malformed. Please go back and try again.</p>
|
||||
<% end %>
|
@ -12,8 +12,12 @@ Rails.application.routes.draw do
|
||||
|
||||
namespace :contributions do
|
||||
root to: 'donations#index'
|
||||
resources :donations, only: ['index', 'create'] do
|
||||
member do
|
||||
get 'confirm_btcpay'
|
||||
end
|
||||
end
|
||||
get 'projects', to: 'projects#index'
|
||||
resources :donations, only: ['index']
|
||||
end
|
||||
|
||||
resources :invitations, only: ['index', 'show', 'create', 'destroy']
|
||||
|
@ -0,0 +1,9 @@
|
||||
class ChangeDonationAmountsAndCurrency < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
rename_column :donations, :amount_usd, :fiat_amount
|
||||
add_column :donations, :fiat_currency, :string, default: "USD"
|
||||
remove_column :donations, :amount_eur, :integer
|
||||
|
||||
Donation.update_all(fiat_currency: 'USD')
|
||||
end
|
||||
end
|
7
db/migrate/20240214121049_add_new_donation_fields.rb
Normal file
7
db/migrate/20240214121049_add_new_donation_fields.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class AddNewDonationFields < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :donations, :donation_method, :string
|
||||
add_column :donations, :payment_method, :string, default: nil
|
||||
add_column :donations, :btcpay_invoice_id, :string, default: nil
|
||||
end
|
||||
end
|
@ -0,0 +1,8 @@
|
||||
class AddPaymentStatusToDonations < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :donations, :payment_status, :string, default: nil
|
||||
add_index :donations, :payment_status
|
||||
|
||||
Donation.completed.update_all payment_status: "settled"
|
||||
end
|
||||
end
|
35
spec/features/contributions/donations_spec.rb
Normal file
35
spec/features/contributions/donations_spec.rb
Normal file
@ -0,0 +1,35 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Donations page', type: :feature do
|
||||
let(:user) { create :user }
|
||||
|
||||
before do
|
||||
login_as user, :scope => :user
|
||||
end
|
||||
|
||||
describe "Donation methods" do
|
||||
scenario "Only BTCPay enabled" do
|
||||
Setting.btcpay_enabled = true
|
||||
Setting.lndhub_enabled = false
|
||||
Setting.opencollective_enabled = false
|
||||
visit contributions_donations_url
|
||||
|
||||
within ".donation-methods" do
|
||||
expect(page).to have_content("Bitcoin")
|
||||
expect(page).not_to have_content("OpenCollective")
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Only OpenCollective enabled" do
|
||||
Setting.btcpay_enabled = false
|
||||
Setting.lndhub_enabled = false
|
||||
Setting.opencollective_enabled = true
|
||||
visit contributions_donations_url
|
||||
|
||||
within ".donation-methods" do
|
||||
expect(page).not_to have_content("Bitcoin")
|
||||
expect(page).to have_content("OpenCollective")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
32
spec/fixtures/btcpay/create_invoice.rb
vendored
Normal file
32
spec/fixtures/btcpay/create_invoice.rb
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"id" => "Q9GBe143MXHkdpZeH4Ftx5",
|
||||
"storeId" => "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount" => "1",
|
||||
"checkoutLink" => "http://10.1.1.163:23001/i/Q9GBe143MXHkdpZeH4Ftx5",
|
||||
"status" => "New",
|
||||
"additionalStatus" => "None",
|
||||
"monitoringExpiration" => 1707995026,
|
||||
"expirationTime" => 1707908626,
|
||||
"createdTime" => 1707907726,
|
||||
"availableStatusesForManualMarking" =>["Settled", "Invalid"],
|
||||
"archived" => false,
|
||||
"type" => "Standard",
|
||||
"currency" => "EUR",
|
||||
"metadata" => {},
|
||||
"checkout" => {
|
||||
"speedPolicy" => "MediumSpeed",
|
||||
"paymentMethods" => ["BTC", "BTC-LightningNetwork"],
|
||||
"defaultPaymentMethod" => "BTC-LightningNetwork",
|
||||
"expirationMinutes" => 15,
|
||||
"monitoringMinutes" => 1440,
|
||||
"paymentTolerance" => 0.0,
|
||||
"redirectURL" => "http://localhost:3000/contributions/donations",
|
||||
"redirectAutomatically" => false,
|
||||
"requiresRefundEmail" => false,
|
||||
"defaultLanguage" => nil,
|
||||
"checkoutType" => nil,
|
||||
"lazyPaymentMethods" => nil},
|
||||
"receipt" => {
|
||||
"enabled" => nil, "showQR" => nil, "showPayments" => nil
|
||||
}
|
||||
}
|
41
spec/fixtures/btcpay/lightning_eur_settled_invoice.json
vendored
Normal file
41
spec/fixtures/btcpay/lightning_eur_settled_invoice.json
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "MCkDbf2cUgBuuisUCgnRnb",
|
||||
"storeId": "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount": "1",
|
||||
"checkoutLink": "http://10.1.1.163:23001/i/MCkDbf2cUgBuuisUCgnRnb",
|
||||
"status": "Settled",
|
||||
"additionalStatus": "None",
|
||||
"monitoringExpiration": 1708169508,
|
||||
"expirationTime": 1708083108,
|
||||
"createdTime": 1708082208,
|
||||
"availableStatusesForManualMarking": [
|
||||
|
||||
],
|
||||
"archived": false,
|
||||
"type": "Standard",
|
||||
"currency": "EUR",
|
||||
"metadata": {
|
||||
},
|
||||
"checkout": {
|
||||
"speedPolicy": "MediumSpeed",
|
||||
"paymentMethods": [
|
||||
"BTC",
|
||||
"BTC-LightningNetwork"
|
||||
],
|
||||
"defaultPaymentMethod": "BTC-LightningNetwork",
|
||||
"expirationMinutes": 15,
|
||||
"monitoringMinutes": 1440,
|
||||
"paymentTolerance": 0.0,
|
||||
"redirectURL": "http://localhost:3000/contributions/donations/27/confirm_btcpay",
|
||||
"redirectAutomatically": true,
|
||||
"requiresRefundEmail": false,
|
||||
"defaultLanguage": null,
|
||||
"checkoutType": null,
|
||||
"lazyPaymentMethods": null
|
||||
},
|
||||
"receipt": {
|
||||
"enabled": null,
|
||||
"showQR": null,
|
||||
"showPayments": null
|
||||
}
|
||||
}
|
46
spec/fixtures/btcpay/lightning_eur_settled_payments.json
vendored
Normal file
46
spec/fixtures/btcpay/lightning_eur_settled_payments.json
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "bc1qtvwjguv679lcch9a9zxzxcengq3t3zgd5zm0pd",
|
||||
"paymentLink": "bitcoin:bc1qtvwjguv679lcch9a9zxzxcengq3t3zgd5zm0pd",
|
||||
"rate": "48532.8",
|
||||
"paymentMethodPaid": "0",
|
||||
"totalPaid": "0.00002061",
|
||||
"due": "0",
|
||||
"amount": "0.00002061",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
|
||||
],
|
||||
"paymentMethod": "BTC",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "lnbc20610n1pju73pqpp5rrvhc34tzt3vz70r33c2n2qqtm6hxau2hyl9w23kvrx56vhsfh5sdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp56grmnp2ezj4n323squu2p2e09g5lgncsr7f34084p5dyjgmwcssq9qyyssqfrc9x37qcvvpsx8m4zvu9glvcfcmqzs9ttfsg30g2gjxfkylvp8rdud2yx8gshs2jv0rea0etjrcygrc0hp4vckgsfs4grsnl854ajgpurzzp4",
|
||||
"paymentLink": "lightning:lnbc20610n1pju73pqpp5rrvhc34tzt3vz70r33c2n2qqtm6hxau2hyl9w23kvrx56vhsfh5sdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp56grmnp2ezj4n323squu2p2e09g5lgncsr7f34084p5dyjgmwcssq9qyyssqfrc9x37qcvvpsx8m4zvu9glvcfcmqzs9ttfsg30g2gjxfkylvp8rdud2yx8gshs2jv0rea0etjrcygrc0hp4vckgsfs4grsnl854ajgpurzzp4",
|
||||
"rate": "48532.8",
|
||||
"paymentMethodPaid": "0.00002061",
|
||||
"totalPaid": "0.00002061",
|
||||
"due": "0",
|
||||
"amount": "0.00002061",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
{
|
||||
"id": "18d97c46ab12e2c179e38c70a9a8005ef573778ab93e572a3660cd4d32f04de9",
|
||||
"receivedDate": 1708082214,
|
||||
"value": "0.00002061",
|
||||
"fee": "0.0",
|
||||
"status": "Settled",
|
||||
"destination": "lnbc20610n1pju73pqpp5rrvhc34tzt3vz70r33c2n2qqtm6hxau2hyl9w23kvrx56vhsfh5sdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp56grmnp2ezj4n323squu2p2e09g5lgncsr7f34084p5dyjgmwcssq9qyyssqfrc9x37qcvvpsx8m4zvu9glvcfcmqzs9ttfsg30g2gjxfkylvp8rdud2yx8gshs2jv0rea0etjrcygrc0hp4vckgsfs4grsnl854ajgpurzzp4"
|
||||
}
|
||||
],
|
||||
"paymentMethod": "BTC-LightningNetwork",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
"paymentHash": "18d97c46ab12e2c179e38c70a9a8005ef573778ab93e572a3660cd4d32f04de9"
|
||||
}
|
||||
}
|
||||
]
|
41
spec/fixtures/btcpay/lightning_sats_settled_invoice.json
vendored
Normal file
41
spec/fixtures/btcpay/lightning_sats_settled_invoice.json
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "JxjfeJi1TtX8FcWSjEvGxg",
|
||||
"storeId": "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount": "0.0001",
|
||||
"checkoutLink": "http://10.1.1.163:23001/i/JxjfeJi1TtX8FcWSjEvGxg",
|
||||
"status": "Settled",
|
||||
"additionalStatus": "None",
|
||||
"monitoringExpiration": 1708180292,
|
||||
"expirationTime": 1708093892,
|
||||
"createdTime": 1708092992,
|
||||
"availableStatusesForManualMarking": [
|
||||
|
||||
],
|
||||
"archived": false,
|
||||
"type": "Standard",
|
||||
"currency": "BTC",
|
||||
"metadata": {
|
||||
},
|
||||
"checkout": {
|
||||
"speedPolicy": "MediumSpeed",
|
||||
"paymentMethods": [
|
||||
"BTC",
|
||||
"BTC-LightningNetwork"
|
||||
],
|
||||
"defaultPaymentMethod": "BTC-LightningNetwork",
|
||||
"expirationMinutes": 15,
|
||||
"monitoringMinutes": 1440,
|
||||
"paymentTolerance": 0.0,
|
||||
"redirectURL": "http://localhost:3000/contributions/donations/32/confirm_btcpay",
|
||||
"redirectAutomatically": true,
|
||||
"requiresRefundEmail": false,
|
||||
"defaultLanguage": null,
|
||||
"checkoutType": null,
|
||||
"lazyPaymentMethods": null
|
||||
},
|
||||
"receipt": {
|
||||
"enabled": null,
|
||||
"showQR": null,
|
||||
"showPayments": null
|
||||
}
|
||||
}
|
46
spec/fixtures/btcpay/lightning_sats_settled_payments.json
vendored
Normal file
46
spec/fixtures/btcpay/lightning_sats_settled_payments.json
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "bc1q9fay59qdmtv46d5hpf62vt5eyd7ag98t4h0s3g",
|
||||
"paymentLink": "bitcoin:bc1q9fay59qdmtv46d5hpf62vt5eyd7ag98t4h0s3g",
|
||||
"rate": "1.0",
|
||||
"paymentMethodPaid": "0",
|
||||
"totalPaid": "0.0001",
|
||||
"due": "0",
|
||||
"amount": "0.0001",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
|
||||
],
|
||||
"paymentMethod": "BTC",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "lnbc100u1pju7mjqpp54yt6z4g4j294vta90yn35pwch76a4h47txx4m4njfdqmcsa4w50qdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5gx3wee4c2gsl7lnmx4qkrha3mkxh926j5qpdqz7wyyya04wks7cq9qyyssq4ft7a69c93kr04hamp5ah958ay222dvdrzr3nl599nx0l3ejpqe4ktarkxdymsxgg6v3evat9e9u0fp2vg2r2z860fn0h04znq9c6psqh8s53q",
|
||||
"paymentLink": "lightning:lnbc100u1pju7mjqpp54yt6z4g4j294vta90yn35pwch76a4h47txx4m4njfdqmcsa4w50qdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5gx3wee4c2gsl7lnmx4qkrha3mkxh926j5qpdqz7wyyya04wks7cq9qyyssq4ft7a69c93kr04hamp5ah958ay222dvdrzr3nl599nx0l3ejpqe4ktarkxdymsxgg6v3evat9e9u0fp2vg2r2z860fn0h04znq9c6psqh8s53q",
|
||||
"rate": "1.0",
|
||||
"paymentMethodPaid": "0.0001",
|
||||
"totalPaid": "0.0001",
|
||||
"due": "0",
|
||||
"amount": "0.0001",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
{
|
||||
"id": "a917a15515928b562fa579271a05d8bfb5dadebe598d5dd6724b41bc43b5751e",
|
||||
"receivedDate": 1708093015,
|
||||
"value": "0.0001",
|
||||
"fee": "0.0",
|
||||
"status": "Settled",
|
||||
"destination": "lnbc100u1pju7mjqpp54yt6z4g4j294vta90yn35pwch76a4h47txx4m4njfdqmcsa4w50qdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5gx3wee4c2gsl7lnmx4qkrha3mkxh926j5qpdqz7wyyya04wks7cq9qyyssq4ft7a69c93kr04hamp5ah958ay222dvdrzr3nl599nx0l3ejpqe4ktarkxdymsxgg6v3evat9e9u0fp2vg2r2z860fn0h04znq9c6psqh8s53q"
|
||||
}
|
||||
],
|
||||
"paymentMethod": "BTC-LightningNetwork",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
"paymentHash": "a917a15515928b562fa579271a05d8bfb5dadebe598d5dd6724b41bc43b5751e"
|
||||
}
|
||||
}
|
||||
]
|
42
spec/fixtures/btcpay/onchain_eur_processing_invoice.json
vendored
Normal file
42
spec/fixtures/btcpay/onchain_eur_processing_invoice.json
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "K4e31MhbLKmr3D7qoNYRd3",
|
||||
"storeId": "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount": "100",
|
||||
"checkoutLink": "http://10.1.1.163:23001/i/K4e31MhbLKmr3D7qoNYRd3",
|
||||
"status": "Processing",
|
||||
"additionalStatus": "None",
|
||||
"monitoringExpiration": 1708173683,
|
||||
"expirationTime": 1708087283,
|
||||
"createdTime": 1708086383,
|
||||
"availableStatusesForManualMarking": [
|
||||
"Settled",
|
||||
"Invalid"
|
||||
],
|
||||
"archived": false,
|
||||
"type": "Standard",
|
||||
"currency": "USD",
|
||||
"metadata": {
|
||||
},
|
||||
"checkout": {
|
||||
"speedPolicy": "MediumSpeed",
|
||||
"paymentMethods": [
|
||||
"BTC",
|
||||
"BTC-LightningNetwork"
|
||||
],
|
||||
"defaultPaymentMethod": "BTC-LightningNetwork",
|
||||
"expirationMinutes": 15,
|
||||
"monitoringMinutes": 1440,
|
||||
"paymentTolerance": 0.0,
|
||||
"redirectURL": "http://localhost:3000/contributions/donations/28/confirm_btcpay",
|
||||
"redirectAutomatically": true,
|
||||
"requiresRefundEmail": false,
|
||||
"defaultLanguage": null,
|
||||
"checkoutType": null,
|
||||
"lazyPaymentMethods": null
|
||||
},
|
||||
"receipt": {
|
||||
"enabled": null,
|
||||
"showQR": null,
|
||||
"showPayments": null
|
||||
}
|
||||
}
|
46
spec/fixtures/btcpay/onchain_eur_processing_payments.json
vendored
Normal file
46
spec/fixtures/btcpay/onchain_eur_processing_payments.json
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh",
|
||||
"paymentLink": "bitcoin:bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh",
|
||||
"rate": "52259.2",
|
||||
"paymentMethodPaid": "0.00191354",
|
||||
"totalPaid": "0.00191354",
|
||||
"due": "0",
|
||||
"amount": "0.00191354",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
{
|
||||
"id": "21da85563274d0c3975273c1a2a8551bddeebb68b8f8a3242f63dd4cc238b480-1",
|
||||
"receivedDate": 1708086448,
|
||||
"value": "0.00191354",
|
||||
"fee": "0.0",
|
||||
"status": "Processing",
|
||||
"destination": "bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh"
|
||||
}
|
||||
],
|
||||
"paymentMethod": "BTC",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "lnbc1913540n1pju74r0pp5vpnw6l84ytu5u5evehn00xwsrppg7w45cj4mrwjwng474w7x3ugqdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5sscc8ufdw29lpv9z09c6edhzc2njg0lmspsk8sdunek7yrkeu3mq9qyyssqnzz9xrhpy7sej5k62vcjju253kxx87jveq7vusl2sgaeuyh48ph9ecuud6f329syuut3z8w544c6ynhtx4ratundzmp7fs6sdll8g0spurjhnx",
|
||||
"paymentLink": "lightning:lnbc1913540n1pju74r0pp5vpnw6l84ytu5u5evehn00xwsrppg7w45cj4mrwjwng474w7x3ugqdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5sscc8ufdw29lpv9z09c6edhzc2njg0lmspsk8sdunek7yrkeu3mq9qyyssqnzz9xrhpy7sej5k62vcjju253kxx87jveq7vusl2sgaeuyh48ph9ecuud6f329syuut3z8w544c6ynhtx4ratundzmp7fs6sdll8g0spurjhnx",
|
||||
"rate": "52259.2",
|
||||
"paymentMethodPaid": "0",
|
||||
"totalPaid": "0.00191354",
|
||||
"due": "0",
|
||||
"amount": "0.00191354",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
|
||||
],
|
||||
"paymentMethod": "BTC-LightningNetwork",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
"paymentHash": "6066ed7cf522f94e532ccde6f799d018428f3ab4c4abb1ba4e9a2beabbc68f10"
|
||||
}
|
||||
}
|
||||
]
|
41
spec/fixtures/btcpay/onchain_eur_settled_invoice.json
vendored
Normal file
41
spec/fixtures/btcpay/onchain_eur_settled_invoice.json
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "K4e31MhbLKmr3D7qoNYRd3",
|
||||
"storeId": "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount": "100",
|
||||
"checkoutLink": "http://10.1.1.163:23001/i/K4e31MhbLKmr3D7qoNYRd3",
|
||||
"status": "Settled",
|
||||
"additionalStatus": "None",
|
||||
"monitoringExpiration": 1708173683,
|
||||
"expirationTime": 1708087283,
|
||||
"createdTime": 1708086383,
|
||||
"availableStatusesForManualMarking": [
|
||||
|
||||
],
|
||||
"archived": false,
|
||||
"type": "Standard",
|
||||
"currency": "USD",
|
||||
"metadata": {
|
||||
},
|
||||
"checkout": {
|
||||
"speedPolicy": "MediumSpeed",
|
||||
"paymentMethods": [
|
||||
"BTC",
|
||||
"BTC-LightningNetwork"
|
||||
],
|
||||
"defaultPaymentMethod": "BTC-LightningNetwork",
|
||||
"expirationMinutes": 15,
|
||||
"monitoringMinutes": 1440,
|
||||
"paymentTolerance": 0.0,
|
||||
"redirectURL": "http://localhost:3000/contributions/donations/28/confirm_btcpay",
|
||||
"redirectAutomatically": true,
|
||||
"requiresRefundEmail": false,
|
||||
"defaultLanguage": null,
|
||||
"checkoutType": null,
|
||||
"lazyPaymentMethods": null
|
||||
},
|
||||
"receipt": {
|
||||
"enabled": null,
|
||||
"showQR": null,
|
||||
"showPayments": null
|
||||
}
|
||||
}
|
46
spec/fixtures/btcpay/onchain_eur_settled_payments.json
vendored
Normal file
46
spec/fixtures/btcpay/onchain_eur_settled_payments.json
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh",
|
||||
"paymentLink": "bitcoin:bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh",
|
||||
"rate": "52259.2",
|
||||
"paymentMethodPaid": "0.00191354",
|
||||
"totalPaid": "0.00191354",
|
||||
"due": "0",
|
||||
"amount": "0.00191354",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
{
|
||||
"id": "218652f351508c46cfd99de1c6cdc0dcb66bc1bbfaf38578235d080046a96305-1",
|
||||
"receivedDate": 1708106396,
|
||||
"value": "0.00191354",
|
||||
"fee": "0.0",
|
||||
"status": "Settled",
|
||||
"destination": "bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh"
|
||||
}
|
||||
],
|
||||
"paymentMethod": "BTC",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "lnbc1913540n1pju74r0pp5vpnw6l84ytu5u5evehn00xwsrppg7w45cj4mrwjwng474w7x3ugqdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5sscc8ufdw29lpv9z09c6edhzc2njg0lmspsk8sdunek7yrkeu3mq9qyyssqnzz9xrhpy7sej5k62vcjju253kxx87jveq7vusl2sgaeuyh48ph9ecuud6f329syuut3z8w544c6ynhtx4ratundzmp7fs6sdll8g0spurjhnx",
|
||||
"paymentLink": "lightning:lnbc1913540n1pju74r0pp5vpnw6l84ytu5u5evehn00xwsrppg7w45cj4mrwjwng474w7x3ugqdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5sscc8ufdw29lpv9z09c6edhzc2njg0lmspsk8sdunek7yrkeu3mq9qyyssqnzz9xrhpy7sej5k62vcjju253kxx87jveq7vusl2sgaeuyh48ph9ecuud6f329syuut3z8w544c6ynhtx4ratundzmp7fs6sdll8g0spurjhnx",
|
||||
"rate": "52259.2",
|
||||
"paymentMethodPaid": "0",
|
||||
"totalPaid": "0.00191354",
|
||||
"due": "0",
|
||||
"amount": "0.00191354",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
|
||||
],
|
||||
"paymentMethod": "BTC-LightningNetwork",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
"paymentHash": "6066ed7cf522f94e532ccde6f799d018428f3ab4c4abb1ba4e9a2beabbc68f10"
|
||||
}
|
||||
}
|
||||
]
|
@ -1,9 +1,4 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ApplicationHelper do
|
||||
describe "sats_to_btc" do
|
||||
it "converts satoshis to BTC" do
|
||||
expect(helper.sats_to_btc(120000000)).to eq(1.2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
79
spec/jobs/btcpay_check_donation_job_spec.rb
Normal file
79
spec/jobs/btcpay_check_donation_job_spec.rb
Normal file
@ -0,0 +1,79 @@
|
||||
require 'rails_helper'
|
||||
require 'webmock/rspec'
|
||||
|
||||
RSpec.describe BtcpayCheckDonationJob, type: :job do
|
||||
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
|
||||
|
||||
let(:donation) do
|
||||
user.donations.create!(
|
||||
donation_method: "btcpay",
|
||||
btcpay_invoice_id: "K4e31MhbLKmr3D7qoNYRd3",
|
||||
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
|
||||
})
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
clear_enqueued_jobs
|
||||
clear_performed_jobs
|
||||
end
|
||||
|
||||
describe "invoice still processing" do
|
||||
subject(:job) { described_class.perform_later(donation) }
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_processing_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_processing_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3")
|
||||
.to_return(status: 200, headers: {}, body: invoice)
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3/payment-methods")
|
||||
.to_return(status: 200, headers: {}, body: payments)
|
||||
end
|
||||
|
||||
it "enqueues itself to check again later" do
|
||||
expect_any_instance_of(described_class).to receive(:re_enqueue_job).once
|
||||
perform_enqueued_jobs { job }
|
||||
end
|
||||
end
|
||||
|
||||
describe "invoice settled" do
|
||||
subject(:job) { described_class.perform_later(donation) }
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_settled_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_settled_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3")
|
||||
.to_return(status: 200, headers: {}, body: invoice)
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3/payment-methods")
|
||||
.to_return(status: 200, headers: {}, body: payments)
|
||||
end
|
||||
|
||||
it "updates the donation record" do
|
||||
perform_enqueued_jobs(only: described_class) { job }
|
||||
donation.reload
|
||||
expect(donation.paid_at).not_to be_nil
|
||||
expect(donation.payment_status).to eq("settled")
|
||||
end
|
||||
|
||||
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
|
||||
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')
|
||||
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
|
||||
end
|
||||
end
|
270
spec/requests/contributions/donations_spec.rb
Normal file
270
spec/requests/contributions/donations_spec.rb
Normal file
@ -0,0 +1,270 @@
|
||||
require 'rails_helper'
|
||||
require 'webmock/rspec'
|
||||
|
||||
RSpec.describe "Donations", type: :request do
|
||||
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
|
||||
|
||||
before do
|
||||
Warden.test_mode!
|
||||
login_as user, scope: :user
|
||||
end
|
||||
|
||||
after { Warden.test_reset! }
|
||||
|
||||
describe "#create" do
|
||||
describe "with disabled methods" do
|
||||
before do
|
||||
Setting.btcpay_enabled = false
|
||||
end
|
||||
|
||||
it "returns a 403" do
|
||||
post "/contributions/donations", params: { donation_method: "btcpay" }
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with fake methods" do
|
||||
it "returns a 403" do
|
||||
post "/contributions/donations", params: { donation_method: "remotestorage" }
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with invalid fiat currency" do
|
||||
it "returns a 422" do
|
||||
post "/contributions/donations", params: {
|
||||
donation_method: "btcpay", amount: "10", currency: "GBP"
|
||||
}
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with bad amount" do
|
||||
it "returns a 422" do
|
||||
post "/contributions/donations", params: {
|
||||
donation_method: "btcpay", amount: ""
|
||||
}
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with BTCPay" do
|
||||
before { Setting.btcpay_enabled = true }
|
||||
|
||||
describe "amount in EUR" do
|
||||
before do
|
||||
expect(BtcpayManager::CreateInvoice).to receive(:call)
|
||||
.with(amount: 25, currency: "EUR", redirect_url: "http://www.example.com/contributions/donations/1/confirm_btcpay")
|
||||
.and_return({
|
||||
"id" => "Q9GBe143HJIkdpZeH4Ftx5",
|
||||
"amount" => "25",
|
||||
"currency" => "EUR",
|
||||
"checkoutLink" => "#{Setting.btcpay_api_url}/i/Q9GBe143HJIkdpZeH4Ftx5",
|
||||
"expirationTime" => 1707908626,
|
||||
"checkout" => { "redirectURL" => "http://www.example.com/contributions/donations/1/confirm_btcpay" }
|
||||
})
|
||||
|
||||
post "/contributions/donations", params: {
|
||||
donation_method: "btcpay", amount: "25", currency: "EUR",
|
||||
public_name: "Mickey"
|
||||
}
|
||||
end
|
||||
|
||||
it "creates a new donation record" do
|
||||
expect(user.donations.count).to eq(1)
|
||||
donation = user.donations.first
|
||||
expect(donation.donation_method).to eq("btcpay")
|
||||
expect(donation.payment_method).to be_nil
|
||||
expect(donation.paid_at).to be_nil
|
||||
expect(donation.public_name).to eq("Mickey")
|
||||
expect(donation.amount_sats).to be_nil
|
||||
expect(donation.fiat_amount).to eq(2500)
|
||||
expect(donation.fiat_currency).to eq("EUR")
|
||||
expect(donation.btcpay_invoice_id).to eq("Q9GBe143HJIkdpZeH4Ftx5")
|
||||
end
|
||||
|
||||
it "redirects to the BTCPay checkout page" do
|
||||
expect(response).to redirect_to("https://btcpay.example.com/i/Q9GBe143HJIkdpZeH4Ftx5")
|
||||
end
|
||||
end
|
||||
|
||||
describe "amount in sats" do
|
||||
before do
|
||||
expect(BtcpayManager::CreateInvoice).to receive(:call)
|
||||
.with(amount: 0.0001, currency: "BTC", redirect_url: "http://www.example.com/contributions/donations/1/confirm_btcpay")
|
||||
.and_return({
|
||||
"id" => "Q9GBe143HJIkdpZeH4Ftx5",
|
||||
"amount" => "0.0001",
|
||||
"currency" => "BTC",
|
||||
"checkoutLink" => "#{Setting.btcpay_api_url}/i/Q9GBe143HJIkdpZeH4Ftx5",
|
||||
"expirationTime" => 1707908626,
|
||||
"checkout" => { "redirectURL" => "http://www.example.com/contributions/donations/1/confirm_btcpay" }
|
||||
})
|
||||
|
||||
post "/contributions/donations", params: {
|
||||
donation_method: "btcpay", amount: "10000", currency: "sats",
|
||||
public_name: "Garret Holmes"
|
||||
}
|
||||
end
|
||||
|
||||
it "creates a new donation record" do
|
||||
expect(user.donations.count).to eq(1)
|
||||
donation = user.donations.first
|
||||
expect(donation.donation_method).to eq("btcpay")
|
||||
expect(donation.payment_method).to be_nil
|
||||
expect(donation.paid_at).to be_nil
|
||||
expect(donation.public_name).to eq("Garret Holmes")
|
||||
expect(donation.amount_sats).to eq(10000)
|
||||
expect(donation.fiat_amount).to be_nil
|
||||
expect(donation.fiat_currency).to be_nil
|
||||
expect(donation.btcpay_invoice_id).to eq("Q9GBe143HJIkdpZeH4Ftx5")
|
||||
end
|
||||
|
||||
it "redirects to the BTCPay checkout page" do
|
||||
expect(response).to redirect_to("https://btcpay.example.com/i/Q9GBe143HJIkdpZeH4Ftx5")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#confirm_btcpay" do
|
||||
before { Setting.btcpay_enabled = true }
|
||||
|
||||
describe "with donation of another user" do
|
||||
let(:other_user) { create :user, id: 3, cn: "carl", ou: 'kosmos.org', email: "carl@example.com" }
|
||||
|
||||
before do
|
||||
@donation = other_user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "123abc",
|
||||
fiat_amount: 25, fiat_currency: "EUR", paid_at: nil
|
||||
)
|
||||
get confirm_btcpay_contributions_donation_path(@donation.id)
|
||||
end
|
||||
|
||||
it "returns a 404" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with confirmed donation" do
|
||||
before do
|
||||
@donation = user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "123abc",
|
||||
fiat_amount: 25, fiat_currency: "EUR",
|
||||
paid_at: "2024-02-16", payment_status: "settled"
|
||||
)
|
||||
get confirm_btcpay_contributions_donation_path(@donation.id)
|
||||
end
|
||||
|
||||
it "redirects to the donations index" do
|
||||
expect(response).to redirect_to(contributions_donations_url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "settled via Lightning" do
|
||||
describe "amount in EUR" do
|
||||
subject do
|
||||
user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "MCkDbf2cUgBuuisUCgnRnb",
|
||||
fiat_amount: 25, fiat_currency: "EUR", paid_at: nil
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/lightning_eur_settled_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/lightning_eur_settled_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/MCkDbf2cUgBuuisUCgnRnb")
|
||||
.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)
|
||||
|
||||
get confirm_btcpay_contributions_donation_path(subject)
|
||||
end
|
||||
|
||||
it "updates the donation record" do
|
||||
subject.reload
|
||||
expect(subject.paid_at).not_to be_nil
|
||||
expect(subject.amount_sats).to eq(2061)
|
||||
end
|
||||
|
||||
it "redirects to the donations index" do
|
||||
expect(response).to redirect_to(contributions_donations_url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "amount in sats" do
|
||||
subject do
|
||||
user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "JxjfeJi1TtX8FcWSjEvGxg",
|
||||
amount_sats: 10000, fiat_amount: nil, fiat_currency: nil, paid_at: nil
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/lightning_sats_settled_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/lightning_sats_settled_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/JxjfeJi1TtX8FcWSjEvGxg")
|
||||
.to_return(status: 200, headers: {}, body: invoice)
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/JxjfeJi1TtX8FcWSjEvGxg/payment-methods")
|
||||
.to_return(status: 200, headers: {}, body: payments)
|
||||
|
||||
expect(BtcpayManager::FetchExchangeRate).to receive(:call)
|
||||
.with(fiat_currency: "EUR").and_return(48532.00)
|
||||
|
||||
get confirm_btcpay_contributions_donation_path(subject)
|
||||
end
|
||||
|
||||
it "updates the donation record" do
|
||||
subject.reload
|
||||
expect(subject.paid_at).not_to be_nil
|
||||
expect(subject.amount_sats).to eq(10000)
|
||||
expect(subject.fiat_amount).to eq(485)
|
||||
expect(subject.fiat_currency).to eq("EUR")
|
||||
end
|
||||
|
||||
it "redirects to the donations index" do
|
||||
expect(response).to redirect_to(contributions_donations_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "on-chain" do
|
||||
describe "waiting for confirmations" do
|
||||
subject do
|
||||
user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "K4e31MhbLKmr3D7qoNYRd3",
|
||||
fiat_amount: 120, fiat_currency: "USD", paid_at: nil
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_processing_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_processing_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3")
|
||||
.to_return(status: 200, headers: {}, body: invoice)
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3/payment-methods")
|
||||
.to_return(status: 200, headers: {}, body: payments)
|
||||
|
||||
get confirm_btcpay_contributions_donation_path(subject)
|
||||
end
|
||||
|
||||
it "updates the donation record" do
|
||||
subject.reload
|
||||
expect(subject.paid_at).to be_nil
|
||||
expect(subject.amount_sats).to eq(191354)
|
||||
expect(subject.payment_status).to eq("processing")
|
||||
end
|
||||
|
||||
it "enqueues a job to periodically check the invoice status" do
|
||||
expect(enqueued_jobs.size).to eq(1)
|
||||
expect(enqueued_jobs.first["job_class"]).to eq("BtcpayCheckDonationJob")
|
||||
expect(enqueued_jobs.first['arguments'][0]["_aj_globalid"]).to eq("gid://akkounts/Donation/#{subject.id}")
|
||||
end
|
||||
|
||||
it "redirects to the donations index" do
|
||||
expect(response).to redirect_to(contributions_donations_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user
Isn't a
amount_sats = params[:amount_sats]
missing here?I couldn't see the
amount_sats
being assigned anywhere other than tonil
when a different currency is selected, but it is being used further down in this action.Good catch! Since I did actual donations using sats for the amount, it must have gotten lost for the commit somehow. Added a spec to catch that, since that spec was missing in the first place.