Implement bitcoin donations via BTCPay

This commit is contained in:
2024-02-14 11:09:03 +01:00
parent 26d613bdca
commit 079ee8833c
46 changed files with 1142 additions and 114 deletions

View 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
View 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
}
}

View 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
}
}

View 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"
}
}
]

View 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
}
}

View 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"
}
}
]

View 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
}
}

View 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"
}
}
]

View 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
}
}

View 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"
}
}
]

View File

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

View File

@@ -0,0 +1,63 @@
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
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 { job }
donation.reload
expect(donation.paid_at).not_to be_nil
expect(donation.payment_status).to eq("settled")
end
it "does not enqueue itself again" do
expect_any_instance_of(described_class).not_to receive(:re_enqueue_job)
perform_enqueued_jobs { job }
end
end
end

View 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