In the past, we didn't have a separate field for the company name, so customers used the billing name as the company name. Now that we've added a company name field, it appears on the invoice twice. In addition, some customers have requested that only the company name be shown on the invoice, not the person's name. Therefore, if the company name is provided, there's no need to include the person's name. We didn't do this before because we were generating invoice PDFs on the fly, which would have altered already generated invoices. However, we have started to persist the invoices in blob storage and display them from there, so these changes won't affect old invoices.
523 lines
25 KiB
Ruby
523 lines
25 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "../spec_helper"
|
|
require "aws-sdk-s3"
|
|
require "pdf-reader"
|
|
require "stripe"
|
|
|
|
RSpec.describe Clover, "billing" do
|
|
let(:user) { create_account }
|
|
let(:project) { user.create_project_with_default_policy("project-1") }
|
|
let(:project_wo_permissions) { user.create_project_with_default_policy("project-2", default_policy: nil) }
|
|
let(:billing_info) do
|
|
bi = BillingInfo.create_with_id(stripe_id: "cs_1234567890")
|
|
project.update(billing_info_id: bi.id)
|
|
bi
|
|
end
|
|
|
|
let(:payment_method) { PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pm_1234567890") }
|
|
|
|
before do
|
|
login(user.email)
|
|
end
|
|
|
|
it "disabled when Stripe secret key not provided" do
|
|
allow(Config).to receive(:stripe_secret_key).and_return(nil)
|
|
|
|
visit project.path
|
|
within "#desktop-menu" do
|
|
expect { click_link "Billing" }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
expect(page.title).to eq("Ubicloud - #{project.name}")
|
|
|
|
visit "#{project.path}/billing"
|
|
expect(page.status_code).to eq(501)
|
|
expect(page.body).to eq "Billing is not enabled. Set STRIPE_SECRET_KEY to enable billing."
|
|
end
|
|
|
|
it "tag payment method fraud after account suspension" do
|
|
expect(payment_method.reload.fraud).to be(false)
|
|
user.suspend
|
|
expect(payment_method.reload.fraud).to be(true)
|
|
end
|
|
|
|
context "when Stripe enabled" do
|
|
before do
|
|
allow(Config).to receive(:stripe_secret_key).and_return("secret_key")
|
|
end
|
|
|
|
it "raises forbidden when does not have permissions" do
|
|
project_wo_permissions
|
|
visit "#{project_wo_permissions.path}/billing"
|
|
|
|
expect(page.title).to eq("Ubicloud - Forbidden")
|
|
expect(page.status_code).to eq(403)
|
|
expect(page).to have_content "Forbidden"
|
|
end
|
|
|
|
it "can create billing info" do
|
|
# rubocop:disable RSpec/VerifiedDoubles
|
|
expect(Stripe::Checkout::Session).to receive(:create).and_return(double(Stripe::Checkout::Session, url: "#{project.path}/billing/success?session_id=session_123"))
|
|
expect(Stripe::PaymentIntent).to receive(:create).and_return(double(Stripe::PaymentIntent, status: "requires_capture", id: "pi_1234567890"))
|
|
# rubocop:enable RSpec/VerifiedDoubles
|
|
expect(Stripe::Checkout::Session).to receive(:retrieve).with("session_123").and_return({"setup_intent" => "st_123456790"})
|
|
expect(Stripe::SetupIntent).to receive(:retrieve).with("st_123456790").and_return({"customer" => "cs_1234567890", "payment_method" => "pm_1234567890"})
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "ACME Inc.", "address" => {"country" => "NL"}, "metadata" => {"company_name" => "Foo Companye Name"}}).twice
|
|
expect(Stripe::PaymentMethod).to receive(:retrieve).with("pm_1234567890").and_return({"card" => {"brand" => "visa"}}).thrice
|
|
|
|
visit project.path
|
|
|
|
within "#desktop-menu" do
|
|
click_link "Billing"
|
|
end
|
|
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
click_button "Add new billing information"
|
|
|
|
billing_info = project.reload.billing_info
|
|
expect(page.status_code).to eq(200)
|
|
expect(billing_info.stripe_id).to eq("cs_1234567890")
|
|
expect(page).to have_field("Billing Name", with: "ACME Inc.")
|
|
expect(billing_info.payment_methods.first.stripe_id).to eq("pm_1234567890")
|
|
expect(page).to have_content "Visa"
|
|
expect(page).to have_no_content "Discount"
|
|
expect(page).to have_no_content "100%"
|
|
|
|
project.this.update(discount: 100)
|
|
page.refresh
|
|
expect(page).to have_content "Discount"
|
|
expect(page).to have_content "100%"
|
|
end
|
|
|
|
it "can not create billing info with unauthorized payment" do
|
|
# rubocop:disable RSpec/VerifiedDoubles
|
|
expect(Stripe::Checkout::Session).to receive(:create).and_return(double(Stripe::Checkout::Session, url: "#{project.path}/billing/success?session_id=session_123"))
|
|
expect(Stripe::PaymentIntent).to receive(:create).and_return(double(Stripe::PaymentIntent, status: "canceled", id: "pi_1234567890"))
|
|
# rubocop:enable RSpec/VerifiedDoubles
|
|
expect(Stripe::Checkout::Session).to receive(:retrieve).with("session_123").and_return({"setup_intent" => "st_123456790"})
|
|
expect(Stripe::SetupIntent).to receive(:retrieve).with("st_123456790").and_return({"customer" => "cs_1234567890", "payment_method" => "pm_1234567890"})
|
|
expect(Stripe::PaymentMethod).to receive(:retrieve).with("pm_1234567890").and_return({"card" => {"brand" => "visa"}}).once
|
|
expect(Clog).to receive(:emit)
|
|
|
|
visit project.path
|
|
|
|
within "#desktop-menu" do
|
|
click_link "Billing"
|
|
end
|
|
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
click_button "Add new billing information"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page).to have_flash_error("We couldn't pre-authorize your card for verification. Please make sure it can be pre-authorized up to $5 or contact our support team at support@ubicloud.com.")
|
|
end
|
|
|
|
it "can update billing info" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with(billing_info.stripe_id).and_return(
|
|
{"name" => "New Inc.", "address" => {"country" => "DE"}, "metadata" => {"tax_id" => "DE456789"}}
|
|
).at_least(:once)
|
|
expect(Stripe::Customer).to receive(:update).with(billing_info.stripe_id, anything)
|
|
visit "#{project.path}/billing"
|
|
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
fill_in "Billing Name", with: "New Inc."
|
|
select "United States", from: "Country"
|
|
|
|
click_button "Update"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page).to have_field("Billing Name", with: "New Inc.")
|
|
expect(page).to have_field("Country", with: "DE")
|
|
end
|
|
|
|
it "can update tax id" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with(billing_info.stripe_id).and_return(
|
|
{"name" => "Old Inc.", "address" => {"country" => "NL"}, "metadata" => {"tax_id" => "123456"}},
|
|
{"name" => "Old Inc.", "address" => {"country" => "NL"}, "metadata" => {"tax_id" => "123456"}},
|
|
{"name" => "New Inc.", "address" => {"country" => "US"}, "metadata" => {"tax_id" => "DE456789"}}
|
|
).at_least(:once)
|
|
expect(Stripe::Customer).to receive(:update).with(billing_info.stripe_id, anything).at_least(:once)
|
|
expect(Strand).to receive(:create).with(prog: "ValidateVat", label: "start", stack: [{subject_id: billing_info.id}])
|
|
visit "#{project.path}/billing"
|
|
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
select "United States", from: "Country"
|
|
fill_in "VAT ID", with: "DE 456-789"
|
|
|
|
click_button "Update"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page).to have_field("Billing Name", with: "New Inc.")
|
|
expect(page).to have_field("Country", with: "US")
|
|
expect(page).to have_field("Tax ID", with: "DE456789")
|
|
|
|
fill_in "Tax ID", with: nil
|
|
|
|
click_button "Update"
|
|
end
|
|
|
|
it "shows error if billing info update failed" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with(billing_info.stripe_id).and_return(
|
|
{"name" => "Old Inc.", "address" => {"country" => "NL"}, "metadata" => {"tax_id" => "123456"}}
|
|
).at_least(:once)
|
|
expect(Stripe::Customer).to receive(:update).and_raise(Stripe::InvalidRequestError.new("Invalid email address: test@test.com", "email"))
|
|
|
|
visit "#{project.path}/billing"
|
|
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
fill_in "Billing Email", with: " test@test.com"
|
|
|
|
click_button "Update"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page).to have_flash_error("Invalid email address: test@test.com")
|
|
end
|
|
|
|
it "can add new payment method" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "ACME Inc.", "address" => {"country" => "NL"}, "metadata" => {"company_name" => "Foo Companye Name"}}).twice
|
|
expect(Stripe::PaymentMethod).to receive(:retrieve).with(payment_method.stripe_id).and_return({"card" => {"brand" => "visa"}}).twice
|
|
expect(Stripe::PaymentMethod).to receive(:retrieve).with("pm_222222222").and_return({"card" => {"brand" => "mastercard"}}).twice
|
|
# rubocop:disable RSpec/VerifiedDoubles
|
|
expect(Stripe::Checkout::Session).to receive(:create).and_return(double(Stripe::Checkout::Session, url: "#{project.path}/billing/success?session_id=session_123"))
|
|
expect(Stripe::PaymentIntent).to receive(:create).and_return(double(Stripe::PaymentIntent, status: "requires_capture", id: "pi_1234567890"))
|
|
# rubocop:enable RSpec/VerifiedDoubles
|
|
expect(Stripe::Checkout::Session).to receive(:retrieve).with("session_123").and_return({"setup_intent" => "st_123456790"})
|
|
expect(Stripe::SetupIntent).to receive(:retrieve).with("st_123456790").and_return({"payment_method" => "pm_222222222"})
|
|
|
|
visit "#{project.path}/billing"
|
|
|
|
click_link "Add Payment Method"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
expect(billing_info.payment_methods.count).to eq(2)
|
|
expect(page).to have_content "Visa"
|
|
expect(page).to have_content "Mastercard"
|
|
end
|
|
|
|
it "can't add fraud payment method" do
|
|
fraud_payment_method = PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pmi_1234567890", fraud: true, card_fingerprint: "cfg1234")
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "ACME Inc.", "address" => {"country" => "NL"}, "metadata" => {"company_name" => "Foo Companye Name"}}).twice
|
|
expect(Stripe::PaymentMethod).to receive(:retrieve).with(fraud_payment_method.stripe_id).and_return({"card" => {"brand" => "visa"}}).twice
|
|
expect(Stripe::PaymentMethod).to receive(:retrieve).with("pm_222222222").and_return({"card" => {"brand" => "mastercard", "fingerprint" => "cfg1234"}})
|
|
# rubocop:disable RSpec/VerifiedDoubles
|
|
expect(Stripe::Checkout::Session).to receive(:create).and_return(double(Stripe::Checkout::Session, url: "#{project.path}/billing/success?session_id=session_123"))
|
|
# rubocop:enable RSpec/VerifiedDoubles
|
|
expect(Stripe::Checkout::Session).to receive(:retrieve).with("session_123").and_return({"setup_intent" => "st_123456790"})
|
|
expect(Stripe::SetupIntent).to receive(:retrieve).with("st_123456790").and_return({"payment_method" => "pm_222222222"})
|
|
|
|
visit "#{project.path}/billing"
|
|
|
|
click_link "Add Payment Method"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
expect(billing_info.payment_methods.count).to eq(1)
|
|
expect(page).to have_content "Visa"
|
|
expect(page).to have_flash_error("Payment method you added is labeled as fraud. Please contact support.")
|
|
end
|
|
|
|
it "raises not found when payment method not exists" do
|
|
visit "#{project.path}/billing/payment-method/08s56d4kaj94xsmrnf5v5m3mav"
|
|
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content "ResourceNotFound"
|
|
end
|
|
|
|
it "raises not found when add payment method if project not exists" do
|
|
visit "#{project.path}/billing/payment-method/create"
|
|
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content "ResourceNotFound"
|
|
end
|
|
|
|
it "can't delete last payment method" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "ACME Inc.", "address" => {"country" => "NL"}, "metadata" => {"company_name" => "Foo Companye Name"}})
|
|
expect(Stripe::PaymentMethod).to receive(:retrieve).with(payment_method.stripe_id).and_return({"card" => {"brand" => "visa"}})
|
|
|
|
visit "#{project.path}/billing"
|
|
|
|
# We send delete request manually instead of just clicking to button because delete action triggered by JavaScript.
|
|
# UI tests run without a JavaScript engine.
|
|
btn = find "#payment-method-#{payment_method.ubid} .delete-btn"
|
|
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
|
|
|
|
expect(page.status_code).to eq(400)
|
|
expect(page.body).to eq({error: {message: "You can't delete the last payment method of a project."}}.to_json)
|
|
expect(billing_info.reload.payment_methods.count).to eq(1)
|
|
end
|
|
|
|
it "can delete payment method" do
|
|
payment_method_2 = PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pm_2222222222")
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "John Doe", "address" => {"country" => "NL"}, "metadata" => {"company_name" => "ACME Inc."}})
|
|
expect(Stripe::PaymentMethod).to receive(:retrieve).with(payment_method.stripe_id).and_return({"card" => {"brand" => "visa"}})
|
|
expect(Stripe::PaymentMethod).to receive(:retrieve).with(payment_method_2.stripe_id).and_return({"card" => {"brand" => "mastercard"}})
|
|
expect(Stripe::PaymentMethod).to receive(:detach).with(payment_method.stripe_id)
|
|
|
|
visit "#{project.path}/billing"
|
|
|
|
# We send delete request manually instead of just clicking to button because delete action triggered by JavaScript.
|
|
# UI tests run without a JavaScript enginer.
|
|
btn = find "#payment-method-#{payment_method.ubid} .delete-btn"
|
|
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
|
|
|
|
expect(page.status_code).to eq(204)
|
|
expect(page.body).to be_empty
|
|
expect(billing_info.reload.payment_methods.count).to eq(1)
|
|
end
|
|
|
|
describe "invoices" do
|
|
let(:blob_storage_client) { Aws::S3::Client.new(stub_responses: true) }
|
|
|
|
before do
|
|
allow(Aws::S3::Client).to receive(:new).and_return(blob_storage_client)
|
|
blob_storage_client.stub_responses(:get_object, "NoSuchKey")
|
|
end
|
|
|
|
def billing_record(begin_time, end_time)
|
|
vm = create_vm
|
|
BillingRecord.create_with_id(
|
|
project_id: billing_info.project.id,
|
|
resource_id: vm.id,
|
|
resource_name: vm.name,
|
|
span: Sequel::Postgres::PGRange.new(begin_time, end_time),
|
|
billing_rate_id: BillingRate.from_resource_properties("VmVCpu", vm.family, vm.location.name)["id"],
|
|
amount: vm.vcpus
|
|
)
|
|
end
|
|
|
|
it "list invoices of project" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with(billing_info.stripe_id).and_return({"name" => "John Doe", "address" => {"country" => "NL"}, "metadata" => {}}).at_least(:once)
|
|
bi = billing_record(Time.utc(2023, 6), Time.utc(2023, 7))
|
|
InvoiceGenerator.new(bi.span.begin, bi.span.end, save_result: true, eur_rate: 1.1).run
|
|
invoice = Invoice.first
|
|
|
|
visit "#{project.path}/billing"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
expect(page).to have_content invoice.name
|
|
|
|
invoice.content["cost"] = 123.45
|
|
invoice.content["subtotal"] = 543.21
|
|
invoice.this.update(content: invoice.content)
|
|
page.refresh
|
|
expect(page).to have_content("$123.45 ($543.21)")
|
|
|
|
click_link invoice.name
|
|
end
|
|
|
|
it "show current usage details" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "John Doe", "address" => {"country" => "NL"}, "metadata" => {"company_name" => "ACME Inc."}}).at_least(:once)
|
|
bi = billing_record(Time.utc(2023, 6), Time.utc(2023, 7))
|
|
100.times do
|
|
billing_record(Time.utc(2023, 6), Time.utc(2023, 6) + 10)
|
|
end
|
|
invoice = InvoiceGenerator.new(bi.span.begin, bi.span.end, save_result: true, eur_rate: 1.1).run.first
|
|
invoice.update(status: "current")
|
|
expect(InvoiceGenerator).to receive(:new).and_return(instance_double(InvoiceGenerator, run: [invoice])).at_least(:once)
|
|
|
|
visit "#{project.path}/billing/invoice/current"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - Current Usage Summary")
|
|
expect(page).to have_content "Aggregated"
|
|
expect(page.has_css?("#invoice-discount")).to be false
|
|
expect(page.has_css?("#invoice-credit")).to be false
|
|
|
|
content = invoice.content
|
|
content["discount"] = 1
|
|
content["credit"] = 2
|
|
content["free_inference_tokens_credit"] = 3
|
|
invoice.this.update(content:)
|
|
|
|
page.refresh
|
|
expect(find_by_id("invoice-discount").text).to eq "-$1.00"
|
|
expect(find_by_id("invoice-credit").text).to eq "-$2.00"
|
|
expect(find_by_id("invoice-free-inference-tokens").text).to eq "-$3.00"
|
|
end
|
|
|
|
it "show current invoice when no usage" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with(billing_info.stripe_id).and_return({"name" => "John Doe", "address" => {}, "metadata" => {}}).at_least(:once)
|
|
|
|
visit "#{project.path}/billing"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
expect(page).to have_content "current"
|
|
expect(page).to have_content "not finalized"
|
|
|
|
click_link href: "#{project.path}/billing/invoice/current"
|
|
expect(page).to have_content "Current Usage Summary"
|
|
expect(page).to have_content "No resources"
|
|
end
|
|
|
|
it "list current invoice with last month usage" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with(billing_info.stripe_id).and_return({"name" => "John Doe", "address" => {"country" => "NL"}, "metadata" => {}}).at_least(:once)
|
|
br_previous = billing_record(Time.utc(2023, 6), Time.utc(2023, 7))
|
|
br_current = billing_record(Time.utc(2023, 7), Time.utc(2023, 7, 15))
|
|
invoice_previous = InvoiceGenerator.new(br_previous.span.begin, br_previous.span.end, save_result: true, eur_rate: 1.1).run.first
|
|
invoice_current = InvoiceGenerator.new(br_current.span.begin, br_current.span.end, project_ids: [project.id]).run.first
|
|
|
|
visit "#{project.path}/billing"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - Project Billing")
|
|
expect(page).to have_content "current"
|
|
expect(page).to have_content "not finalized"
|
|
expect(page).to have_content "$%0.02f" % invoice_current.content["cost"]
|
|
expect(page).to have_content "$%0.02f" % invoice_previous.content["cost"]
|
|
|
|
click_link href: "#{project.path}/billing/invoice/current"
|
|
expect(page).to have_content "Current Usage Summary"
|
|
expect(page).to have_content "$%0.02f" % invoice_current.content["cost"]
|
|
expect(page).to have_content "less than $0.001"
|
|
end
|
|
|
|
it "show finalized invoice as PDF from US issuer without VAT" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "John Doe", "address" => {"country" => "US"}, "metadata" => {"company_name" => "Acme Inc.", "tax_id" => "123123123"}}).at_least(:once)
|
|
bi = billing_record(Time.utc(2023, 6), Time.utc(2023, 7))
|
|
invoice = InvoiceGenerator.new(bi.span.begin, bi.span.end, save_result: true, eur_rate: 1.1).run.first
|
|
|
|
visit "#{project.path}/billing"
|
|
click_link invoice.name
|
|
|
|
expect(page.status_code).to eq(200)
|
|
text = PDF::Reader.new(StringIO.new(page.body)).pages.map(&:text).join(" ")
|
|
expect(text).to include("Ubicloud Inc.")
|
|
expect(text).to include("Acme Inc.")
|
|
expect(text).not_to include("John Doe")
|
|
expect(text).to include("test-vm")
|
|
expect(text).not_to include("VAT")
|
|
end
|
|
|
|
it "show finalized invoice as PDF from EU issuer with 21% VAT" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "John Doe", "address" => {"country" => "DE"}, "metadata" => {"company_name" => ""}}).at_least(:once)
|
|
bi = billing_record(Time.utc(2023, 6), Time.utc(2023, 7))
|
|
invoice = InvoiceGenerator.new(bi.span.begin, bi.span.end, save_result: true, eur_rate: 1.1).run.first
|
|
|
|
visit "#{project.path}/billing"
|
|
expect(page).to have_content "EU registered business can enter their VAT ID to remove VAT from future invoices."
|
|
click_link invoice.name
|
|
|
|
expect(page.status_code).to eq(200)
|
|
text = PDF::Reader.new(StringIO.new(page.body)).pages.map(&:text).join(" ")
|
|
expect(text).to include("Ubicloud B.V.")
|
|
expect(text).to include("John Doe")
|
|
expect(text).to include("test-vm")
|
|
expect(text).to include("VAT (21%): (€5.68) $5.17")
|
|
end
|
|
|
|
it "show finalized invoice as PDF from EU issuer with reversed charge" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "ACME Inc.", "address" => {"country" => "DE"}, "metadata" => {"tax_id" => "123123123"}}).at_least(:once)
|
|
bi = billing_record(Time.utc(2023, 6), Time.utc(2023, 7))
|
|
invoice = InvoiceGenerator.new(bi.span.begin, bi.span.end, save_result: true, eur_rate: 1.1).run.first
|
|
|
|
visit "#{project.path}/billing"
|
|
expect(page).to have_content "VAT subject to reverse charge."
|
|
click_link invoice.name
|
|
|
|
expect(page.status_code).to eq(200)
|
|
text = PDF::Reader.new(StringIO.new(page.body)).pages.map(&:text).join(" ")
|
|
expect(text).to include("Ubicloud B.V.")
|
|
expect(text).to include("test-vm")
|
|
expect(text).to include("VAT subject to reverse charge")
|
|
end
|
|
|
|
it "show finalized invoice as PDF with old issuer info" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "John Doe", "address" => {"country" => "US"}, "metadata" => {"tax_id" => "123123123"}}).at_least(:once)
|
|
bi = billing_record(Time.utc(2023, 6), Time.utc(2023, 7))
|
|
invoice = InvoiceGenerator.new(bi.span.begin, bi.span.end, save_result: true, eur_rate: 1.1).run.first
|
|
invoice.content["issuer_info"].merge!("name" => nil, "tax_id" => "123123123", "in_eu_vat" => false)
|
|
invoice.modified!(:content)
|
|
invoice.save_changes
|
|
|
|
visit "#{project.path}/billing/invoice/#{invoice.ubid}"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
text = PDF::Reader.new(StringIO.new(page.body)).pages.map(&:text).join(" ")
|
|
expect(text).to include("John Doe")
|
|
expect(text).to include("test-vm")
|
|
expect(text).to include("Tax ID: 123123123")
|
|
end
|
|
|
|
it "show persisted invoice PDF from blob storage" do
|
|
expect(Stripe::Customer).to receive(:retrieve).with("cs_1234567890").and_return({"name" => "John Doe", "address" => {"country" => "US"}, "metadata" => {"company_name" => "ACME Inc.", "tax_id" => "123123123"}}).at_least(:once)
|
|
bi = billing_record(Time.utc(2023, 6), Time.utc(2023, 7))
|
|
invoice = InvoiceGenerator.new(bi.span.begin, bi.span.end, save_result: true, eur_rate: 1.1).run.first
|
|
pdf = invoice.generate_pdf(Serializers::Invoice.serialize(invoice, {detailed: true}))
|
|
response = instance_double(Aws::S3::Types::GetObjectOutput, body: instance_double(StringIO, read: pdf))
|
|
expect(blob_storage_client).to receive(:get_object).with(bucket: "ubicloud-invoices", key: invoice.blob_key).and_return(response)
|
|
|
|
visit "#{project.path}/billing"
|
|
click_link invoice.name
|
|
|
|
expect(page.status_code).to eq(200)
|
|
text = PDF::Reader.new(StringIO.new(page.body)).pages.map(&:text).join(" ")
|
|
expect(text).to include("Ubicloud Inc.")
|
|
expect(text).to include("ACME Inc.")
|
|
end
|
|
|
|
it "raises not found when invoice not exists" do
|
|
visit "#{project.path}/billing/invoice/08s56d4kaj94xsmrnf5v5m3mav"
|
|
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content "ResourceNotFound"
|
|
end
|
|
end
|
|
|
|
describe "usage alerts" do
|
|
before do
|
|
UsageAlert.create_with_id(project_id: project.id, user_id: user.id, name: "alert-1", limit: 101)
|
|
UsageAlert.create_with_id(project_id: project_wo_permissions.id, user_id: user.id, name: "alert-2", limit: 100)
|
|
end
|
|
|
|
it "can list usage alerts" do
|
|
visit "#{project.path}/billing"
|
|
expect(page).to have_content "alert-1"
|
|
expect(page).to have_no_content "alert-2"
|
|
end
|
|
|
|
it "can create usage alert" do
|
|
visit "#{project.path}/billing"
|
|
fill_in "alert_name", with: "alert-3"
|
|
fill_in "limit", with: 200
|
|
click_button "Add"
|
|
|
|
expect(page).to have_content "alert-3"
|
|
end
|
|
|
|
it "can delete usage alert" do
|
|
visit "#{project.path}/billing"
|
|
expect(page).to have_content "alert-1"
|
|
|
|
# We send delete request manually instead of just clicking to button because delete action triggered by JavaScript.
|
|
# UI tests run without a JavaScript engine.
|
|
btn = find "#alert-#{project.usage_alerts.first.ubid} .delete-btn"
|
|
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
|
|
|
|
visit "#{project.path}/billing"
|
|
expect(page).to have_flash_notice "Usage alert alert-1 is deleted."
|
|
|
|
visit "#{project.path}/billing"
|
|
expect(page).to have_no_content "alert-1"
|
|
end
|
|
|
|
it "returns 404 if usage alert not found" do
|
|
visit project.path + "/billing"
|
|
expect(page).to have_content "alert-1"
|
|
|
|
btn = find "#alert-#{project.usage_alerts.first.ubid} .delete-btn"
|
|
|
|
project.usage_alerts.first.destroy
|
|
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
|
|
|
|
expect(page.status_code).to eq(404)
|
|
end
|
|
end
|
|
end
|
|
end
|