Merge pull request 'Let users donate sats via BTCPay Server' (#176) from feature/donations_btcpay into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #176 Reviewed-by: galfert <garret.alfert@gmail.com>
This commit is contained in:
commit
8b897168cc
@ -31,6 +31,7 @@
|
|||||||
# Service Integrations
|
# Service Integrations
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||||
# BTCPAY_API_URL='http://localhost:23001/api/v1'
|
# BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||||
# BTCPAY_STORE_ID=''
|
# BTCPAY_STORE_ID=''
|
||||||
# BTCPAY_AUTH_TOKEN=''
|
# BTCPAY_AUTH_TOKEN=''
|
||||||
|
@ -2,6 +2,7 @@ PRIMARY_DOMAIN=kosmos.org
|
|||||||
|
|
||||||
REDIS_URL='redis://localhost:6379/0'
|
REDIS_URL='redis://localhost:6379/0'
|
||||||
|
|
||||||
|
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||||
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||||
BTCPAY_STORE_ID='123456'
|
BTCPAY_STORE_ID='123456'
|
||||||
|
|
||||||
|
@ -32,6 +32,11 @@
|
|||||||
focus:ring-blue-400 focus:ring-opacity-75;
|
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 {
|
.btn-red {
|
||||||
@apply bg-red-600 hover:bg-red-700 text-white
|
@apply bg-red-600 hover:bg-red-700 text-white
|
||||||
focus:ring-red-500 focus:ring-opacity-75;
|
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">
|
<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">
|
<div class="px-6 sm:px-12 pt-2 sm:pt-4">
|
||||||
<%= render partial: @tabnav_partial %>
|
<%= render partial: @tabnav_partial %>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<!-- Modal Container -->
|
<!-- Modal Container -->
|
||||||
<div data-modal-target="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">
|
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
|
||||||
<!-- Modal Card -->
|
<!-- Modal Card -->
|
||||||
<div class="m-1 bg-white rounded shadow">
|
<div class="m-1 bg-white rounded shadow">
|
||||||
|
@ -34,6 +34,8 @@ class NotificationComponent < ViewComponent::Base
|
|||||||
'alert-octagon'
|
'alert-octagon'
|
||||||
when 'alert'
|
when 'alert'
|
||||||
'alert-octagon'
|
'alert-octagon'
|
||||||
|
when 'warning'
|
||||||
|
'alert-octagon'
|
||||||
else
|
else
|
||||||
'info'
|
'info'
|
||||||
end
|
end
|
||||||
|
@ -3,18 +3,16 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
before_action :set_current_section, only: [:index, :show, :new, :edit]
|
before_action :set_current_section, only: [:index, :show, :new, :edit]
|
||||||
|
|
||||||
# GET /donations
|
# GET /donations
|
||||||
# GET /donations.json
|
|
||||||
def index
|
def index
|
||||||
@pagy, @donations = pagy(Donation.all.order('created_at desc'))
|
@pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
|
||||||
|
|
||||||
@stats = {
|
@stats = {
|
||||||
overall_sats: @donations.all.sum("amount_sats"),
|
overall_sats: @donations.sum("amount_sats"),
|
||||||
donor_count: Donation.distinct.count(:user_id)
|
donor_count: @donations.distinct.count(:user_id)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /donations/1
|
# GET /donations/1
|
||||||
# GET /donations/1.json
|
|
||||||
def show
|
def show
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -28,54 +26,41 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
# POST /donations
|
# POST /donations
|
||||||
# POST /donations.json
|
|
||||||
def create
|
def create
|
||||||
@donation = Donation.new(donation_params)
|
@donation = Donation.new(donation_params)
|
||||||
|
|
||||||
respond_to do |format|
|
if @donation.paid_at == nil
|
||||||
if @donation.save
|
@donation.errors.add(:paid_at, message: "is required")
|
||||||
format.html do
|
render :new, status: :unprocessable_entity and return
|
||||||
redirect_to admin_donation_url(@donation), flash: {
|
end
|
||||||
success: 'Donation was successfully created.'
|
|
||||||
}
|
if @donation.save
|
||||||
end
|
redirect_to admin_donation_url(@donation), flash: {
|
||||||
format.json { render :show, status: :created, location: @donation }
|
success: 'Donation was successfully created.'
|
||||||
else
|
}
|
||||||
format.html { render :new, status: :unprocessable_entity }
|
else
|
||||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
render :new, status: :unprocessable_entity
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# PATCH/PUT /donations/1
|
# PUT /donations/1
|
||||||
# PATCH/PUT /donations/1.json
|
|
||||||
def update
|
def update
|
||||||
respond_to do |format|
|
if @donation.update(donation_params)
|
||||||
if @donation.update(donation_params)
|
redirect_to admin_donation_url(@donation), flash: {
|
||||||
format.html do
|
success: 'Donation was successfully updated.'
|
||||||
redirect_to admin_donation_url(@donation), flash: {
|
}
|
||||||
success: 'Donation was successfully updated.'
|
else
|
||||||
}
|
render :edit, status: :unprocessable_entity
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# DELETE /donations/1
|
# DELETE /donations/1
|
||||||
# DELETE /donations/1.json
|
|
||||||
def destroy
|
def destroy
|
||||||
@donation.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.'
|
success: 'Donation was successfully destroyed.'
|
||||||
}
|
}
|
||||||
end
|
|
||||||
format.json { head :no_content }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -86,7 +71,10 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
|
|
||||||
# Only allow a list of trusted parameters through.
|
# Only allow a list of trusted parameters through.
|
||||||
def donation_params
|
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
|
end
|
||||||
|
|
||||||
def set_current_section
|
def set_current_section
|
||||||
|
@ -41,4 +41,26 @@ class ApplicationController < ActionController::Base
|
|||||||
def after_sign_in_path_for(user)
|
def after_sign_in_path_for(user)
|
||||||
session[:user_return_to] || root_path
|
session[:user_return_to] || root_path
|
||||||
end
|
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
|
end
|
||||||
|
@ -1,10 +1,129 @@
|
|||||||
class Contributions::DonationsController < ApplicationController
|
class Contributions::DonationsController < ApplicationController
|
||||||
before_action :authenticate_user!
|
include BtcpayHelper
|
||||||
|
|
||||||
# GET /donations
|
before_action :authenticate_user!
|
||||||
# GET /donations.json
|
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
|
def index
|
||||||
@donations = current_user.donations.completed
|
|
||||||
@current_section = :contributions
|
@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
|
end
|
||||||
|
|
||||||
|
# POST /contributions/donations
|
||||||
|
def create
|
||||||
|
if params[:currency] == "sats"
|
||||||
|
fiat_amount = nil
|
||||||
|
fiat_currency = nil
|
||||||
|
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
|
end
|
||||||
|
@ -2,10 +2,11 @@ require "rqrcode"
|
|||||||
require "lnurl"
|
require "lnurl"
|
||||||
|
|
||||||
class Services::LightningController < ApplicationController
|
class Services::LightningController < ApplicationController
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :authenticate_with_lndhub
|
|
||||||
before_action :set_current_section
|
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
|
def index
|
||||||
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
@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
|
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
|
def set_current_section
|
||||||
@current_section = :services
|
@current_section = :services
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_balance
|
def require_service_available
|
||||||
lndhub = Lndhub.new
|
http_status :not_found unless Setting.lndhub_enabled?
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_transactions
|
def fetch_transactions
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
include Pagy::Frontend
|
include Pagy::Frontend
|
||||||
|
|
||||||
def sats_to_btc(sats)
|
|
||||||
sats.to_f / 100000000
|
|
||||||
end
|
|
||||||
|
|
||||||
def main_nav_class(current_section, link_to_section)
|
def main_nav_class(current_section, link_to_section)
|
||||||
if 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"
|
"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"
|
@subject = "New invitations added to your account"
|
||||||
mail to: @user.email, subject: @subject
|
mail to: @user.email, subject: @subject
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bitcoin_donation_confirmed
|
||||||
|
@user = params[:user]
|
||||||
|
@donation = params[:donation]
|
||||||
|
@subject = "Donation confirmed"
|
||||||
|
mail to: @user.email, subject: @subject
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,12 +4,25 @@ class Donation < ApplicationRecord
|
|||||||
|
|
||||||
# Validations
|
# Validations
|
||||||
validates_presence_of :user
|
validates_presence_of :user
|
||||||
validates_presence_of :amount_sats
|
validates_presence_of :donation_method,
|
||||||
validates_presence_of :paid_at
|
inclusion: { in: %w[ custom btcpay lndhub ] }
|
||||||
|
validates_presence_of :payment_status, allow_nil: true,
|
||||||
# Hooks
|
inclusion: { in: %w[ processing settled ] }
|
||||||
# TODO before_create :store_fiat_value
|
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
|
#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
|
end
|
||||||
|
@ -51,6 +51,9 @@ class Setting < RailsSettings::Base
|
|||||||
field :btcpay_enabled, type: :boolean,
|
field :btcpay_enabled, type: :boolean,
|
||||||
default: ENV["BTCPAY_API_URL"].present?
|
default: ENV["BTCPAY_API_URL"].present?
|
||||||
|
|
||||||
|
field :btcpay_public_url, type: :string,
|
||||||
|
default: ENV["BTCPAY_PUBLIC_URL"].presence
|
||||||
|
|
||||||
field :btcpay_store_id, type: :string,
|
field :btcpay_store_id, type: :string,
|
||||||
default: ENV["BTCPAY_STORE_ID"].presence
|
default: ENV["BTCPAY_STORE_ID"].presence
|
||||||
|
|
||||||
@ -157,7 +160,13 @@ class Setting < RailsSettings::Base
|
|||||||
# Nostr
|
# Nostr
|
||||||
#
|
#
|
||||||
|
|
||||||
field :nostr_enabled, type: :boolean, default: true
|
field :nostr_enabled, type: :boolean, default: false
|
||||||
|
|
||||||
|
#
|
||||||
|
# OpenCollective
|
||||||
|
#
|
||||||
|
|
||||||
|
field :opencollective_enabled, type: :boolean, default: true
|
||||||
|
|
||||||
#
|
#
|
||||||
# RemoteStorage
|
# 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
|
module BtcpayManager
|
||||||
class FetchLightningWalletBalance < BtcpayManagerService
|
class FetchLightningWalletBalance < BtcpayManagerService
|
||||||
def call
|
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
|
confirmed_balance: res["offchain"]["local"].to_i / 1000 # msats to sats
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
module BtcpayManager
|
module BtcpayManager
|
||||||
class FetchOnchainWalletBalance < BtcpayManagerService
|
class FetchOnchainWalletBalance < BtcpayManagerService
|
||||||
def call
|
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
|
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
|
||||||
|
@ -2,23 +2,35 @@
|
|||||||
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
|
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
|
||||||
#
|
#
|
||||||
class BtcpayManagerService < ApplicationService
|
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
|
private
|
||||||
|
|
||||||
def get(endpoint)
|
def base_url
|
||||||
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
|
@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",
|
"Content-Type" => "application/json",
|
||||||
"Accept" => "application/json",
|
"Accept" => "application/json",
|
||||||
"Authorization" => "token #{auth_token}"
|
"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)
|
JSON.parse(res.body)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
class Lndhub
|
class Lndhub < ApplicationService
|
||||||
attr_accessor :auth_token
|
attr_accessor :auth_token
|
||||||
|
|
||||||
def initialize
|
def post(path, payload)
|
||||||
@base_url = ENV["LNDHUB_API_URL"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def post(endpoint, payload)
|
|
||||||
headers = { "Content-Type" => "application/json" }
|
headers = { "Content-Type" => "application/json" }
|
||||||
if auth_token
|
if auth_token
|
||||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||||
end
|
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
|
log_error(res) if res.status != 200
|
||||||
|
|
||||||
JSON.parse(res.body)
|
JSON.parse(res.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(endpoint, auth_token)
|
def get(path, auth_token)
|
||||||
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
|
res = Faraday.get(endpoint_url(path), {}, {
|
||||||
"Content-Type" => "application/json",
|
"Content-Type" => "application/json",
|
||||||
"Accept" => "application/json",
|
"Accept" => "application/json",
|
||||||
"Authorization" => "Bearer #{auth_token}"
|
"Authorization" => "Bearer #{auth_token}"
|
||||||
@ -42,7 +38,7 @@ class Lndhub
|
|||||||
self.auth_token
|
self.auth_token
|
||||||
end
|
end
|
||||||
|
|
||||||
def balance(user_token=nil)
|
def fetch_balance(user_token=nil)
|
||||||
get "balance", user_token || auth_token
|
get "balance", user_token || auth_token
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -72,4 +68,14 @@ class Lndhub
|
|||||||
Sentry.capture_message("Lndhub API request failed: #{res.body}")
|
Sentry.capture_message("Lndhub API request failed: #{res.body}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def base_url
|
||||||
|
@base_url ||= Setting.lndhub_api_url
|
||||||
|
end
|
||||||
|
|
||||||
|
def endpoint_url(path)
|
||||||
|
"#{base_url}/#{path.gsub(/^\//, '')}"
|
||||||
|
end
|
||||||
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
|
class LndhubV2 < Lndhub
|
||||||
|
|
||||||
def post(endpoint, payload, options={})
|
def post(path, payload, options={})
|
||||||
headers = { "Content-Type" => "application/json" }
|
headers = { "Content-Type" => "application/json" }
|
||||||
if auth_token
|
if auth_token
|
||||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||||
elsif options[:admin_token]
|
elsif options[:admin_token]
|
||||||
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
|
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
|
||||||
end
|
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
|
log_error(res) if res.status != 200
|
||||||
|
|
||||||
JSON.parse(res.body)
|
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.label :user_id %>
|
||||||
<%= form.collection_select :user_id, User.where(ou: Setting.primary_domain).order(:cn), :id, :cn, {} %>
|
<%= 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.label :amount_sats, "Amount BTC (sats)" %>
|
||||||
<%= form.number_field :amount_sats %>
|
<%= form.number_field :amount_sats %>
|
||||||
|
|
||||||
<%= form.label :amount_eur, "Amount EUR (cents)" %>
|
<%= form.label :fiat_amount, "Fiat Amount (cents)" %>
|
||||||
<%= form.number_field :amount_eur %>
|
<%= form.number_field :fiat_amount %>
|
||||||
|
|
||||||
<%= form.label :amount_usd, "Amount USD (cents)"%>
|
<%= form.label :fiat_currency, "Fiat Currency" %>
|
||||||
<%= form.number_field :amount_usd %>
|
<%= form.select :fiat_currency, options_for_select([
|
||||||
|
["EUR", "EUR"], ["USD", "USD"], ["sats", "sats"]
|
||||||
|
], selected: donation.fiat_currency) %>
|
||||||
|
|
||||||
<%= form.label :public_name %>
|
<%= form.label :public_name %>
|
||||||
<%= form.text_field :public_name %>
|
<%= form.text_field :public_name %>
|
||||||
|
@ -25,9 +25,8 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th class="text-right">Amount BTC</th>
|
<th class="text-right">Sats</th>
|
||||||
<th class="text-right">in EUR</th>
|
<th class="text-right">Fiat Amount</th>
|
||||||
<th class="text-right">in USD</th>
|
|
||||||
<th class="pl-2">Public name</th>
|
<th class="pl-2">Public name</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@ -37,9 +36,8 @@
|
|||||||
<% @donations.each do |donation| %>
|
<% @donations.each do |donation| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
|
<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_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></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.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td>
|
||||||
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
|
|
||||||
<td class="pl-2"><%= donation.public_name %></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><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
|
@ -1 +0,0 @@
|
|||||||
json.array! @donations, partial: "donations/donation", as: :donation
|
|
@ -8,17 +8,17 @@
|
|||||||
<th>User</th>
|
<th>User</th>
|
||||||
<td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
|
<td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Donation Method</th>
|
||||||
|
<td><%= @donation.donation_method %></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Amount sats</th>
|
<th>Amount sats</th>
|
||||||
<td><%= @donation.amount_sats %></td>
|
<td><%= @donation.amount_sats %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Amount EUR</th>
|
<th>Fiat amount</th>
|
||||||
<td><%= @donation.amount_eur %></td>
|
<td><% if @donation.fiat_amount.present? %><%= number_to_currency @donation.fiat_amount.to_f / 100, unit: "" %> <%= @donation.fiat_currency %><% end %></td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Amount USD</th>
|
|
||||||
<td><%= @donation.amount_usd %></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Public name</th>
|
<th>Public name</th>
|
||||||
@ -26,7 +26,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 %>
|
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
|
||||||
<section>
|
<section>
|
||||||
<% if @donations.any? %>
|
<p class="mb-12">
|
||||||
<p class="mb-12">
|
Your financial contributions to the development and upkeep of Kosmos
|
||||||
Your financial contributions to the development and upkeep of Kosmos
|
software and services.
|
||||||
software and services.
|
</p>
|
||||||
</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
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<% 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>
|
</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 %>
|
||||||
|
<% if @donation_methods.include?(:opencollective) %>
|
||||||
|
<%= render partial: "contributions/donations/opencollective" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</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 %>
|
<% 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
|
namespace :contributions do
|
||||||
root to: 'donations#index'
|
root to: 'donations#index'
|
||||||
|
resources :donations, only: ['index', 'create'] do
|
||||||
|
member do
|
||||||
|
get 'confirm_btcpay'
|
||||||
|
end
|
||||||
|
end
|
||||||
get 'projects', to: 'projects#index'
|
get 'projects', to: 'projects#index'
|
||||||
resources :donations, only: ['index']
|
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :invitations, only: ['index', 'show', 'create', 'destroy']
|
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'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe ApplicationHelper do
|
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
|
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