Compare commits
21 Commits
f57fff0087
...
54220019bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
54220019bb
|
|||
|
079ee8833c
|
|||
|
26d613bdca
|
|||
|
69b3afb8f7
|
|||
|
fee951c05c
|
|||
|
822a2dc018
|
|||
|
5b7fc3707b
|
|||
| 0e2dc54dc6 | |||
| 87f09c94d0 | |||
|
b33b8104a8
|
|||
| 4a4a222973 | |||
| 8c524abcf5 | |||
|
a852ab75ae
|
|||
|
de1f234c15
|
|||
|
bd1b177993
|
|||
|
3f110995a4
|
|||
|
a7410058fa
|
|||
|
411587456b
|
|||
|
84e915ece9
|
|||
|
70ac3b0a70
|
|||
|
a7cbd8ce36
|
@@ -31,6 +31,7 @@ WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||
# 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;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<% if @image_url %>
|
||||
<%= image_tag @image_url, class: "h-full w-full" %>
|
||||
<% else %>
|
||||
<%= render partial: "icons/remotestorage", locals: { custom_class: "h-full w-full p-0.5 text-gray-200" } %>
|
||||
<% end %>
|
||||
21
app/components/app_catalog/web_app_icon_component.rb
Normal file
21
app/components/app_catalog/web_app_icon_component.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AppCatalog
|
||||
class WebAppIconComponent < ViewComponent::Base
|
||||
def initialize(web_app:)
|
||||
if web_app&.icon&.attached?
|
||||
@image_url = image_url_for(web_app.icon)
|
||||
elsif web_app&.apple_touch_icon&.attached?
|
||||
@image_url = image_url_for(web_app.apple_touch_icon)
|
||||
end
|
||||
end
|
||||
|
||||
def image_url_for(attachment)
|
||||
if Setting.s3_enabled?
|
||||
s3_image_url(attachment)
|
||||
else
|
||||
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="h-16 w-16 flex-none">
|
||||
<% if @web_app.icon.attached? %>
|
||||
<%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %>
|
||||
<% elsif @web_app.apple_touch_icon.attached? %>
|
||||
<%= image_tag s3_image_url(@web_app.apple_touch_icon), class: "h-full w-full" %>
|
||||
<% else %>
|
||||
<%= render partial: "icons/remotestorage", locals: { custom_class: "h-full w-full p-0.5 text-gray-200" } %>
|
||||
<% end %>
|
||||
<%= render AppCatalog::WebAppIconComponent.new(web_app: @web_app) %>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h4 class="mb-1 text-lg font-bold">
|
||||
<%= @web_app.name %>
|
||||
<%= @web_app&.name || @auth.app_name %>
|
||||
</h4>
|
||||
<p class="text-sm text-gray-500">
|
||||
<%= @auth.client_id %>
|
||||
|
||||
@@ -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,54 +26,41 @@ class Admin::DonationsController < Admin::BaseController
|
||||
end
|
||||
|
||||
# POST /donations
|
||||
# POST /donations.json
|
||||
def create
|
||||
@donation = Donation.new(donation_params)
|
||||
|
||||
respond_to do |format|
|
||||
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
|
||||
if @donation.paid_at == nil
|
||||
@donation.errors.add(:paid_at, message: "is required")
|
||||
render :new, status: :unprocessable_entity and return
|
||||
end
|
||||
|
||||
if @donation.save
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully created.'
|
||||
}
|
||||
else
|
||||
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
|
||||
if @donation.update(donation_params)
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully updated.'
|
||||
}
|
||||
else
|
||||
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: {
|
||||
success: 'Donation was successfully destroyed.'
|
||||
}
|
||||
end
|
||||
format.json { head :no_content }
|
||||
end
|
||||
|
||||
redirect_to admin_donations_url, flash: {
|
||||
success: 'Donation was successfully destroyed.'
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
@@ -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,128 @@
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class AppCatalog::WebApp < ApplicationRecord
|
||||
store :metadata, coder: JSON
|
||||
|
||||
has_many :remote_storage_authorizations
|
||||
has_many :remote_storage_authorizations, dependent: :destroy
|
||||
|
||||
has_one_attached :icon
|
||||
has_one_attached :apple_touch_icon
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,6 +15,9 @@ class Setting < RailsSettings::Base
|
||||
field :redis_url, type: :string,
|
||||
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
||||
|
||||
field :s3_enabled, type: :boolean,
|
||||
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
||||
|
||||
#
|
||||
# Registrations
|
||||
#
|
||||
@@ -48,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
|
||||
|
||||
@@ -154,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
|
||||
|
||||
@@ -18,6 +18,10 @@ module AppCatalogManager
|
||||
@app.metadata[prop] = metadata.send(prop) if prop
|
||||
end
|
||||
|
||||
@app.save!
|
||||
|
||||
# TODO move icon downloads to separate, async job
|
||||
|
||||
if icon = metadata.select_icon(sizes: "256x256") ||
|
||||
icon = metadata.select_icon(sizes: "192x192")
|
||||
attach_remote_image(:icon, icon)
|
||||
@@ -27,8 +31,6 @@ module AppCatalogManager
|
||||
if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon")
|
||||
attach_remote_image(:apple_touch_icon, apple_touch_icon)
|
||||
end
|
||||
|
||||
@app.save!
|
||||
rescue Manifique::Error => e
|
||||
msg = "Fetching web app manifest failed for #{e.url}: #{e.type}"
|
||||
Rails.logger.warn(msg)
|
||||
@@ -42,14 +44,19 @@ module AppCatalogManager
|
||||
else
|
||||
download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}"
|
||||
end
|
||||
filename = "#{attachment_name}.png"
|
||||
key = "web_apps/#{@app.id}/icons/#{attachment_name}.png"
|
||||
filename = "#{attachment_name}-#{Time.now.to_i}.png"
|
||||
key = "web_apps/#{@app.id}/icons/#{filename}"
|
||||
|
||||
begin
|
||||
tempfile = Down.download(download_url)
|
||||
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
|
||||
rescue Down::NotFound
|
||||
Rails.logger.warn "Icon download failed: NotFound error for #{download_url}"
|
||||
msg = "Download of \"#{attachment_name}\" failed: NotFound error for #{download_url}"
|
||||
Rails.logger.warn(msg)
|
||||
Sentry.capture_message(msg)
|
||||
rescue => e
|
||||
Rails.logger.warn "Saving attachment \"#{attachment_name}\" failed: \"#{e.message}\""
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
<% 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 %>
|
||||
<p class="mb-12">
|
||||
Your financial contributions to the development and upkeep of Kosmos
|
||||
software and services.
|
||||
</p>
|
||||
</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 %>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" class="<%= custom_class %>" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" version="1.1" viewBox="0 0 250 249.9" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="24" height="24" class="icon-remotestorage <%= custom_class %>" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" version="1.1" viewBox="0 0 250 249.9" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-66.822 -.16484)">
|
||||
<polygon id="polygon1" fill="currentColor" transform="matrix(.29308 0 0 .29308 83.528 -.028385)" points="228 181 370 100 511 181 652 263 370 425 87 263 87 263 0 213 0 213 0 311 0 378 0 427 0 476 86 525 185 582 370 689 554 582 653 525 653 590 653 592 370 754 0 542 0 640 185 747 370 853 554 747 739 640 739 525 739 476 739 427 739 378 653 427 370 589 86 427 86 361 185 418 370 524 554 418 653 361 739 311 739 213 554 107 370 0 185 107 58 180 144 230"/>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 867 B |
@@ -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!
|
||||
@@ -31,6 +31,7 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if Flipper.enabled?(:avatar_upload, current_user) %>
|
||||
<label class="block">
|
||||
<p class="font-bold mb-1">
|
||||
Avatar
|
||||
@@ -56,6 +57,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<% end %>
|
||||
|
||||
<p class="mt-8 pt-6 border-t border-gray-200 text-right">
|
||||
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||
|
||||
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 %>
|
||||
@@ -71,7 +71,7 @@ Rails.application.configure do
|
||||
# Allow requests from any IP
|
||||
config.web_console.permissions = '0.0.0.0/0'
|
||||
|
||||
if ENV["S3_ENABLED"]
|
||||
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
||||
config.active_storage.service = :s3
|
||||
else
|
||||
config.active_storage.service = :local
|
||||
|
||||
@@ -110,7 +110,7 @@ Rails.application.configure do
|
||||
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
||||
config.action_mailer.raise_delivery_errors = true
|
||||
|
||||
if ENV["S3_ENABLED"]
|
||||
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
||||
config.active_storage.service = :s3
|
||||
else
|
||||
config.active_storage.service = :local
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
local:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("storage") %>
|
||||
root: <%= ENV["ACTIVE_STORAGE_PATH"] || Rails.root.join("storage") %>
|
||||
|
||||
test:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("tmp/storage") %>
|
||||
|
||||
<% if ENV["S3_ENABLED"] %>
|
||||
<% if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false" %>
|
||||
s3:
|
||||
service: S3
|
||||
endpoint: <%= ENV["S3_ENDPOINT"] %>
|
||||
|
||||
@@ -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
|
||||
10
db/schema.rb
10
db/schema.rb
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_02_07_080515) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_02_16_124640) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -50,12 +50,16 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_07_080515) do
|
||||
create_table "donations", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.integer "amount_sats"
|
||||
t.integer "amount_eur"
|
||||
t.integer "amount_usd"
|
||||
t.integer "fiat_amount"
|
||||
t.string "public_name"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "paid_at", precision: nil
|
||||
t.string "fiat_currency", default: "USD"
|
||||
t.string "donation_method"
|
||||
t.string "payment_method"
|
||||
t.string "btcpay_invoice_id"
|
||||
t.string "payment_status"
|
||||
t.index ["user_id"], name: "index_donations_on_user_id"
|
||||
end
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
ldap:
|
||||
image: 4teamwork/389ds:latest
|
||||
volumes:
|
||||
- ./tmp/389ds:/data
|
||||
- 389ds-data:/data
|
||||
networks:
|
||||
- external_network
|
||||
- internal_network
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
volumes:
|
||||
- ./tmp/redis:/data
|
||||
- redis-data:/data
|
||||
|
||||
web:
|
||||
build: .
|
||||
@@ -42,8 +42,10 @@ services:
|
||||
LDAP_ADMIN_PASSWORD: passthebutter
|
||||
LDAP_USE_TLS: "false"
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
ACTIVE_STORAGE_PATH: "/akkounts/tmp/attachments"
|
||||
RS_REDIS_URL: redis://redis:6379/1
|
||||
RS_STORAGE_URL: "http://localhost:4567"
|
||||
S3_ENABLED: false
|
||||
depends_on:
|
||||
- ldap
|
||||
- redis
|
||||
@@ -67,6 +69,7 @@ services:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RS_REDIS_URL: redis://redis:6379/1
|
||||
RS_STORAGE_URL: "http://localhost:4567"
|
||||
S3_ENABLED: false
|
||||
depends_on:
|
||||
- ldap
|
||||
- redis
|
||||
@@ -81,7 +84,7 @@ services:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./tmp/minio:/data
|
||||
- minio-data:/data
|
||||
|
||||
liquor-cabinet:
|
||||
image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-beta.2
|
||||
@@ -116,3 +119,11 @@ networks:
|
||||
external_network:
|
||||
internal_network:
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
389ds-data:
|
||||
driver: local
|
||||
minio-data:
|
||||
driver: local
|
||||
redis-data:
|
||||
driver: local
|
||||
|
||||
11
spec/components/app_catalog/web_app_icon_component_spec.rb
Normal file
11
spec/components/app_catalog/web_app_icon_component_spec.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe AppCatalog::WebAppIconComponent, type: :component do
|
||||
describe "No web app given" do
|
||||
it "renders the default icon" do
|
||||
expect(
|
||||
render_inline(described_class.new(web_app: nil)) {}.to_html
|
||||
).to include("icon-remotestorage")
|
||||
end
|
||||
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
|
||||
@@ -12,6 +12,8 @@ RSpec.describe 'Profile settings', type: :feature do
|
||||
uid: user.cn, ou: user.ou, display_name: "Mark"
|
||||
})
|
||||
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
|
||||
|
||||
Flipper.enable "avatar_upload"
|
||||
end
|
||||
|
||||
feature "Update display name" do
|
||||
|
||||
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
|
||||
233
spec/requests/contributions/donations_spec.rb
Normal file
233
spec/requests/contributions/donations_spec.rb
Normal file
@@ -0,0 +1,233 @@
|
||||
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
|
||||
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
|
||||
Reference in New Issue
Block a user