Billing: fatture manuali, profilo obbligatorio e piani Stripe mensile/annuale.
Rimuove link al portale Stripe in area cliente, aggiunge flusso admin per PDF fattura e email, blocca checkout senza dati di fatturazione, allinea prezzi a €4,90/€39,90 e €9,90/€69,90, locale italiano e documentazione deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
8
backend/spec/helpers/application_helper_spec.rb
Normal file
8
backend/spec/helpers/application_helper_spec.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe ApplicationHelper, type: :helper do
|
||||
it "formatta data e ora in italiano (fuso Rome)" do
|
||||
time = Time.zone.parse("2026-07-02 13:25:00")
|
||||
expect(helper.l_local(time)).to eq("02 luglio 2026 alle 13:25")
|
||||
end
|
||||
end
|
||||
@@ -1,38 +1,70 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Public::BillingHelper, type: :helper do
|
||||
before { load Rails.root.join("db/seeds/plans.rb") }
|
||||
before do
|
||||
load Rails.root.join("db/seeds/plans.rb")
|
||||
allow(MatchLiveTv).to receive(:stripe_enabled?).and_return(true)
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_yearly_price_id).and_return("py")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_monthly_price_id).and_return("pm")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_yearly_price_id).and_return("fy")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_monthly_price_id).and_return("fm")
|
||||
end
|
||||
|
||||
let(:premium_light) { Plan["premium_light"] }
|
||||
let(:premium_full) { Plan["premium_full"] }
|
||||
let(:free_plan) { Plan["free"] }
|
||||
|
||||
it "segna il piano corrente" do
|
||||
action = helper.plan_billing_action(
|
||||
current_slug: "premium_light",
|
||||
target_plan: premium_light,
|
||||
stripe_subscription_active: true
|
||||
)
|
||||
expect(action[:kind]).to eq(:current)
|
||||
end
|
||||
|
||||
it "propone attivazione da free" do
|
||||
it "propone due opzioni checkout da free" do
|
||||
action = helper.plan_billing_action(
|
||||
current_slug: "free",
|
||||
target_plan: premium_light,
|
||||
stripe_subscription_active: false
|
||||
)
|
||||
expect(action[:kind]).to eq(:checkout)
|
||||
expect(action[:label]).to include("Attiva")
|
||||
expect(action[:kind]).to eq(:checkout_options)
|
||||
expect(action[:intervals]).to eq(%w[monthly yearly])
|
||||
end
|
||||
|
||||
it "propone upgrade con abbonamento attivo" do
|
||||
it "segna piano corrente quando non ci sono altri intervalli" do
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_monthly_price_id).and_return(nil)
|
||||
action = helper.plan_billing_action(
|
||||
current_slug: "premium_light",
|
||||
target_plan: premium_light,
|
||||
stripe_subscription_active: true,
|
||||
current_interval: "yearly"
|
||||
)
|
||||
expect(action[:kind]).to eq(:current)
|
||||
expect(action[:label]).to include("€39,90/anno")
|
||||
end
|
||||
|
||||
it "propone cambio intervallo sullo stesso piano" do
|
||||
action = helper.plan_billing_action(
|
||||
current_slug: "premium_light",
|
||||
target_plan: premium_light,
|
||||
stripe_subscription_active: true,
|
||||
current_interval: "yearly"
|
||||
)
|
||||
expect(action[:kind]).to eq(:interval_switch)
|
||||
expect(action[:intervals]).to eq(["monthly"])
|
||||
end
|
||||
|
||||
it "propone upgrade con opzioni intervallo" do
|
||||
action = helper.plan_billing_action(
|
||||
current_slug: "premium_light",
|
||||
target_plan: premium_full,
|
||||
stripe_subscription_active: true
|
||||
stripe_subscription_active: true,
|
||||
current_interval: "yearly"
|
||||
)
|
||||
expect(action[:kind]).to eq(:change)
|
||||
expect(action[:label]).to include("Passa a")
|
||||
expect(action[:kind]).to eq(:change_options)
|
||||
expect(action[:intervals].size).to eq(2)
|
||||
end
|
||||
|
||||
it "stile annuale pieno e mensile outline" do
|
||||
expect(helper.plan_interval_button_class("yearly")).to include("btn-primary")
|
||||
expect(helper.plan_interval_button_class("yearly")).not_to include("btn-outline")
|
||||
expect(helper.plan_interval_button_class("monthly")).to include("btn-outline")
|
||||
expect(helper.plan_interval_button_class("monthly")).not_to include("btn-primary")
|
||||
end
|
||||
|
||||
it "mostra prima il pulsante annuale" do
|
||||
expect(helper.plan_intervals_for_display(%w[monthly yearly])).to eq(%w[yearly monthly])
|
||||
end
|
||||
end
|
||||
|
||||
19
backend/spec/models/billing/invoice_display_spec.rb
Normal file
19
backend/spec/models/billing/invoice_display_spec.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Billing::Invoice do
|
||||
let(:club) { Club.create!(name: "Inv Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||
|
||||
it "non mostra Aruba nel numero in UI" do
|
||||
invoice = club.billing_invoices.build(
|
||||
number: "ARUBA-RUNNER/2026",
|
||||
issued_on: Date.current,
|
||||
amount_cents: 5000,
|
||||
currency: "eur",
|
||||
status: "issued",
|
||||
source: "aruba"
|
||||
)
|
||||
|
||||
expect(invoice.display_number).to eq("RUNNER/2026")
|
||||
expect(invoice.display_number).not_to match(/aruba/i)
|
||||
end
|
||||
end
|
||||
@@ -7,11 +7,14 @@ RSpec.describe ClubBillingProfile do
|
||||
sport: "volleyball",
|
||||
primary_color: "#e53935",
|
||||
secondary_color: "#ffffff",
|
||||
billing_entity_type: "company",
|
||||
billing_legal_name: "ASD Test",
|
||||
billing_email: "billing@test.it",
|
||||
billing_address_line: "Via Roma 1",
|
||||
billing_city: "Milano",
|
||||
billing_province: "MI",
|
||||
billing_postal_code: "20100",
|
||||
billing_country: "IT",
|
||||
billing_vat_number: "12345678901",
|
||||
billing_recipient_code: "ABCDEFG"
|
||||
)
|
||||
@@ -26,4 +29,16 @@ RSpec.describe ClubBillingProfile do
|
||||
club.billing_pec = nil
|
||||
expect(club.billing_profile_complete?).to be false
|
||||
end
|
||||
|
||||
it "requires P.IVA for companies" do
|
||||
club.billing_vat_number = nil
|
||||
expect(club.billing_profile_complete?).to be false
|
||||
expect(club.billing_profile_errors).to include("Partita IVA")
|
||||
end
|
||||
|
||||
it "requires Codice Fiscale for individuals" do
|
||||
club.update!(billing_entity_type: "individual", billing_vat_number: nil, billing_fiscal_code: nil)
|
||||
club.billing_fiscal_code = "RSSMRA80A01H501U"
|
||||
expect(club.billing_profile_complete?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,9 +6,9 @@ RSpec.describe "Public club billing", type: :request do
|
||||
end
|
||||
let!(:club) do
|
||||
c = Club.create!(name: "Billing HTTP Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff",
|
||||
billing_legal_name: "ASD Billing", billing_email: "bill@test.it", billing_address_line: "Via 1",
|
||||
billing_city: "Milano", billing_postal_code: "20100", billing_vat_number: "12345678901",
|
||||
billing_recipient_code: "ABCDEFG")
|
||||
billing_entity_type: "company", billing_legal_name: "ASD Billing", billing_email: "bill@test.it",
|
||||
billing_address_line: "Via 1", billing_city: "Milano", billing_province: "MI", billing_postal_code: "20100",
|
||||
billing_country: "IT", billing_vat_number: "12345678901", billing_recipient_code: "ABCDEFG")
|
||||
ClubMembership.create!(user: coach, club: c, role: "owner")
|
||||
c.teams.create!(name: "U15", sport: "volleyball")
|
||||
load Rails.root.join("db/seeds/plans.rb")
|
||||
@@ -23,21 +23,25 @@ RSpec.describe "Public club billing", type: :request do
|
||||
it "mostra pagamenti e fatture in billing" do
|
||||
club.billing_payments.create!(
|
||||
amount_cents: 4000, currency: "eur", status: "paid", paid_at: Time.current,
|
||||
description: "Premium Light", stripe_invoice_id: "in_spec_1"
|
||||
description: "Premium Light", stripe_invoice_id: "in_spec_#{SecureRandom.hex(6)}"
|
||||
)
|
||||
payment = club.billing_payments.reload.first
|
||||
club.billing_invoices.create!(
|
||||
number: "2026/100", issued_on: Date.current, amount_cents: 4000, status: "draft"
|
||||
number: "2026/100", issued_on: Date.current, amount_cents: 4000, status: "draft",
|
||||
source: "manual", billing_payment: payment
|
||||
)
|
||||
|
||||
login!
|
||||
get public_club_billing_path(club)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include("Pagamenti effettuati")
|
||||
expect(response.body).to include("Fatture")
|
||||
expect(response.body).not_to include("Aruba")
|
||||
expect(response.body).to include("Pagamenti e fatture")
|
||||
expect(response.body).not_to include("Ricevuta Stripe")
|
||||
expect(response.body).not_to include("Sincronizza pagamenti")
|
||||
expect(response.body).not_to include("Gestisci fatturazione su Stripe")
|
||||
expect(response.body).to include("Premium Light")
|
||||
expect(response.body).to include("2026/100")
|
||||
expect(response.body).to include("In preparazione")
|
||||
end
|
||||
|
||||
it "salva profilo fatturazione" do
|
||||
@@ -57,6 +61,89 @@ RSpec.describe "Public club billing", type: :request do
|
||||
login!
|
||||
get public_club_checkout_path(club, plan: "premium_light")
|
||||
|
||||
expect(response).to redirect_to(public_club_billing_profile_path(club))
|
||||
expect(response).to redirect_to(public_club_billing_profile_path(club, plan: "premium_light"))
|
||||
end
|
||||
|
||||
it "non mostra pulsanti checkout se profilo incompleto" do
|
||||
club.update!(billing_vat_number: nil)
|
||||
allow(MatchLiveTv).to receive(:stripe_enabled?).and_return(true)
|
||||
|
||||
login!
|
||||
get public_club_billing_path(club)
|
||||
|
||||
expect(response.body).to include("Completa dati di fatturazione")
|
||||
expect(response.body).not_to include("Attiva Premium Light — €39,90/anno")
|
||||
end
|
||||
|
||||
it "dopo profilo completo reindirizza al checkout" do
|
||||
club.update!(
|
||||
billing_entity_type: "company",
|
||||
billing_vat_number: nil,
|
||||
billing_fiscal_code: nil,
|
||||
billing_recipient_code: nil,
|
||||
billing_pec: nil
|
||||
)
|
||||
login!
|
||||
patch public_club_billing_profile_path(club, plan: "premium_light", interval: "yearly"), params: {
|
||||
club: {
|
||||
billing_entity_type: "company",
|
||||
billing_legal_name: "ASD Billing",
|
||||
billing_vat_number: "12345678901",
|
||||
billing_email: "bill@test.it",
|
||||
billing_address_line: "Via 1",
|
||||
billing_city: "Milano",
|
||||
billing_province: "MI",
|
||||
billing_postal_code: "20100",
|
||||
billing_country: "IT",
|
||||
billing_recipient_code: "ABCDEFG"
|
||||
}
|
||||
}
|
||||
|
||||
expect(response).to redirect_to(public_club_checkout_path(club, plan: "premium_light", interval: "yearly"))
|
||||
end
|
||||
|
||||
it "mostra pulsanti per intervallo e avvia checkout annuale" do
|
||||
allow(MatchLiveTv).to receive(:stripe_enabled?).and_return(true)
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_yearly_price_id).and_return("price_y")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_monthly_price_id).and_return("price_m")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_yearly_price_id).and_return("price_fy")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_monthly_price_id).and_return("price_fm")
|
||||
|
||||
login!
|
||||
get public_club_billing_path(club)
|
||||
expect(response.body).to include("Attiva Premium Light — €39,90/anno")
|
||||
expect(response.body).to include("Attiva Premium Light — €4,90/mese")
|
||||
|
||||
allow(Billing::Stripe::CheckoutSession).to receive(:new).and_return(
|
||||
instance_double(Billing::Stripe::CheckoutSession, url: "https://checkout.stripe.com/test")
|
||||
)
|
||||
get public_club_checkout_path(club, plan: "premium_light", interval: "yearly")
|
||||
expect(response).to redirect_to("https://checkout.stripe.com/test")
|
||||
expect(Billing::Stripe::CheckoutSession).to have_received(:new).with(
|
||||
hash_including(club: club, plan_slug: "premium_light", interval: "yearly")
|
||||
)
|
||||
end
|
||||
|
||||
it "sincronizza il piano al ritorno da checkout success" do
|
||||
allow(MatchLiveTv).to receive(:stripe_enabled?).and_return(true)
|
||||
allow(Billing::Stripe::FinalizeSubscription).to receive(:call).and_return(false)
|
||||
allow(Billing::Stripe::CheckoutSync).to receive(:call) do |**kwargs|
|
||||
Billing::AssignPlan.call(
|
||||
club: kwargs[:club].reload,
|
||||
plan_slug: "premium_light",
|
||||
status: "active",
|
||||
stripe_attrs: { stripe_subscription_id: "sub_ui", billing_interval: "monthly" }
|
||||
)
|
||||
end
|
||||
|
||||
login!
|
||||
get public_club_billing_path(club, checkout: "success", session_id: "cs_test")
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(club.reload.subscription.plan.slug).to eq("premium_light")
|
||||
expect(response.body).to include("Piano attuale: <strong>Premium Light</strong>")
|
||||
expect(Billing::Stripe::CheckoutSync).to have_received(:call).with(
|
||||
club: club, session_id: "cs_test"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
41
backend/spec/services/billing/issue_invoice_spec.rb
Normal file
41
backend/spec/services/billing/issue_invoice_spec.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Billing::IssueInvoice do
|
||||
let(:club) do
|
||||
Club.create!(
|
||||
name: "Mail Club", sport: "volleyball",
|
||||
primary_color: "#e53935", secondary_color: "#ffffff",
|
||||
billing_legal_name: "ASD Mail", billing_email: "fatture@test.it",
|
||||
billing_address_line: "Via 1", billing_city: "Milano", billing_postal_code: "20100",
|
||||
billing_vat_number: "12345678901", billing_recipient_code: "ABCDEFG"
|
||||
)
|
||||
end
|
||||
let(:payment) do
|
||||
club.billing_payments.create!(
|
||||
amount_cents: 500, currency: "eur", status: "paid", paid_at: Time.current,
|
||||
description: "Premium Light — €5/mese"
|
||||
)
|
||||
end
|
||||
let(:invoice) do
|
||||
club.billing_invoices.create!(
|
||||
number: "2026/99", issued_on: Date.current, amount_cents: 500,
|
||||
currency: "eur", status: "draft", source: "manual", billing_payment: payment
|
||||
)
|
||||
end
|
||||
it "emette la fattura, allega il pdf e invia email" do
|
||||
pdf_io = StringIO.new("%PDF-1.4 test\n")
|
||||
|
||||
expect do
|
||||
described_class.call(
|
||||
invoice: invoice,
|
||||
pdf: { io: pdf_io, filename: "fattura-test.pdf", content_type: "application/pdf" }
|
||||
)
|
||||
end.to change { ActionMailer::Base.deliveries.size }.by(1)
|
||||
|
||||
invoice.reload
|
||||
expect(invoice.status).to eq("sent")
|
||||
expect(invoice.pdf).to be_attached
|
||||
expect(invoice.emailed_at).to be_present
|
||||
expect(ActionMailer::Base.deliveries.last.to).to include("fatture@test.it")
|
||||
end
|
||||
end
|
||||
28
backend/spec/services/billing/payment_description_spec.rb
Normal file
28
backend/spec/services/billing/payment_description_spec.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Billing::PaymentDescription do
|
||||
before { load Rails.root.join("db/seeds/plans.rb") }
|
||||
|
||||
it "etichetta da piano e intervallo Stripe" do
|
||||
invoice = double(
|
||||
amount_paid: 500,
|
||||
metadata: { "plan_slug" => "premium_light", "billing_interval" => "monthly" },
|
||||
lines: double(data: [])
|
||||
)
|
||||
|
||||
expect(described_class.for_stripe_invoice(invoice)).to eq("Premium Light — €5/mese")
|
||||
end
|
||||
|
||||
it "ignora descrizioni di test nel pagamento salvato" do
|
||||
payment = Billing::Payment.new(
|
||||
description: "Webhook test",
|
||||
plan_slug: "premium_light",
|
||||
amount_cents: 4000,
|
||||
currency: "eur",
|
||||
status: "paid",
|
||||
club: Club.create!(name: "C", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff")
|
||||
)
|
||||
|
||||
expect(payment.display_description).to eq("Premium Light — €40/anno")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Billing::Stripe::CancelSubscription do
|
||||
let(:club) do
|
||||
Club.create!(
|
||||
name: "Cancel Club", sport: "volleyball",
|
||||
primary_color: "#e53935", secondary_color: "#ffffff",
|
||||
billing_legal_name: "ASD", billing_email: "b@test.it",
|
||||
billing_address_line: "Via 1", billing_city: "Milano", billing_postal_code: "20100",
|
||||
billing_vat_number: "12345678901", billing_recipient_code: "ABCDEFG"
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
load Rails.root.join("db/seeds/plans.rb")
|
||||
allow(MatchLiveTv).to receive(:stripe_enabled?).and_return(true)
|
||||
Billing::AssignPlan.call(
|
||||
club: club,
|
||||
plan_slug: "premium_light",
|
||||
status: "active",
|
||||
stripe_attrs: {
|
||||
stripe_subscription_id: "sub_cancel",
|
||||
stripe_customer_id: "cus_x",
|
||||
current_period_end: 1.month.from_now,
|
||||
billing_interval: "monthly"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it "programma la disdetta a fine periodo su Stripe" do
|
||||
stripe_sub = double(
|
||||
id: "sub_cancel",
|
||||
customer: "cus_x",
|
||||
status: "active",
|
||||
cancel_at_period_end: true,
|
||||
metadata: { "club_id" => club.id, "plan_slug" => "premium_light", "billing_interval" => "monthly" },
|
||||
items: double(data: [double(price: double(id: "price_m", recurring: double(interval: "month")))])
|
||||
)
|
||||
allow(Stripe::Subscription).to receive(:update).with("sub_cancel", cancel_at_period_end: true).and_return(stripe_sub)
|
||||
allow(Billing::Stripe::CheckoutSync).to receive(:from_subscription)
|
||||
allow(Billing::Stripe::SubscriptionPeriod).to receive(:times).and_return([Time.current, 1.month.from_now])
|
||||
|
||||
described_class.call(club: club)
|
||||
|
||||
expect(Stripe::Subscription).to have_received(:update).with("sub_cancel", cancel_at_period_end: true)
|
||||
expect(Billing::Stripe::CheckoutSync).to have_received(:from_subscription).with(stripe_sub, club: club)
|
||||
end
|
||||
|
||||
it "rifiuta se la disdetta è già programmata" do
|
||||
club.subscription.update!(cancel_at_period_end: true)
|
||||
expect { described_class.call(club: club) }.to raise_error(/già programmata/)
|
||||
end
|
||||
end
|
||||
@@ -15,10 +15,10 @@ RSpec.describe Billing::Stripe::ChangePlan do
|
||||
}
|
||||
)
|
||||
allow(MatchLiveTv).to receive(:stripe_enabled?).and_return(true)
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_price_id).and_return("price_light")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_price_id).and_return("price_full")
|
||||
Plan.find_by!(slug: "premium_light").update!(stripe_price_id: "price_light")
|
||||
Plan.find_by!(slug: "premium_full").update!(stripe_price_id: "price_full")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_yearly_price_id).and_return("price_light_y")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_monthly_price_id).and_return("price_light_m")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_yearly_price_id).and_return("price_full_y")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_monthly_price_id).and_return("price_full_m")
|
||||
end
|
||||
|
||||
it "aggiorna Stripe e il piano locale verso premium_full" do
|
||||
@@ -37,21 +37,22 @@ RSpec.describe Billing::Stripe::ChangePlan do
|
||||
allow(Stripe::Subscription).to receive(:retrieve).with("sub_test_123").and_return(stripe_sub)
|
||||
allow(Stripe::Subscription).to receive(:update).and_return(updated_sub)
|
||||
|
||||
described_class.call(club: club, plan_slug: "premium_full")
|
||||
described_class.call(club: club, plan_slug: "premium_full", interval: "yearly")
|
||||
|
||||
expect(Stripe::Subscription).to have_received(:update).with(
|
||||
"sub_test_123",
|
||||
hash_including(
|
||||
items: [{ id: "si_test", price: "price_full" }],
|
||||
metadata: { club_id: club.id, plan_slug: "premium_full" }
|
||||
items: [{ id: "si_test", price: "price_full_y" }],
|
||||
metadata: { club_id: club.id, plan_slug: "premium_full", billing_interval: "yearly" }
|
||||
)
|
||||
)
|
||||
expect(club.reload.subscription.plan.slug).to eq("premium_full")
|
||||
end
|
||||
|
||||
it "rifiuta il cambio sullo stesso piano" do
|
||||
it "rifiuta il cambio sullo stesso piano e intervallo" do
|
||||
club.subscription.update!(billing_interval: "yearly")
|
||||
expect {
|
||||
described_class.call(club: club, plan_slug: "premium_light")
|
||||
described_class.call(club: club, plan_slug: "premium_light", interval: "yearly")
|
||||
}.to raise_error(/già su questo piano/)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,49 +15,38 @@ RSpec.describe Billing::Stripe::CheckoutSession do
|
||||
before do
|
||||
load Rails.root.join("db/seeds/plans.rb")
|
||||
allow(MatchLiveTv).to receive(:stripe_enabled?).and_return(true)
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_price_id).and_return("price_light")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_price_id).and_return("price_full")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_yearly_price_id).and_return("price_light_y")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_monthly_price_id).and_return("price_light_m")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_yearly_price_id).and_return("price_full_y")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_monthly_price_id).and_return("price_full_m")
|
||||
allow(MatchLiveTv).to receive(:app_public_url).and_return("http://localhost:3000")
|
||||
Plan.find_by!(slug: "premium_light").update!(stripe_price_id: "price_light")
|
||||
Plan.find_by!(slug: "premium_full").update!(stripe_price_id: "price_full")
|
||||
end
|
||||
|
||||
it "crea checkout senza subscription esistente" do
|
||||
session = double(url: "https://checkout.stripe.com/test")
|
||||
it "crea checkout annuale" do
|
||||
session = double(url: "https://checkout.stripe.com/yearly")
|
||||
expect(Stripe::Customer).to receive(:create).and_return(double(id: "cus_new"))
|
||||
expect(Billing::Stripe::CustomerSync).to receive(:call).with(club: club, customer_id: "cus_new")
|
||||
expect(Stripe::Checkout::Session).to receive(:create).with(
|
||||
hash_including(
|
||||
mode: "subscription",
|
||||
customer: "cus_new",
|
||||
line_items: [{ price: "price_light", quantity: 1 }],
|
||||
subscription_data: { metadata: { club_id: club.id, plan_slug: "premium_light" } }
|
||||
line_items: [{ price: "price_light_y", quantity: 1 }],
|
||||
metadata: hash_including(plan_slug: "premium_light", billing_interval: "yearly"),
|
||||
subscription_data: { metadata: hash_including(billing_interval: "yearly") }
|
||||
)
|
||||
).and_return(session)
|
||||
|
||||
url = described_class.new(club: club, user: user, plan_slug: "premium_light").url
|
||||
expect(url).to eq("https://checkout.stripe.com/test")
|
||||
url = described_class.new(club: club, user: user, plan_slug: "premium_light", interval: "yearly").url
|
||||
expect(url).to eq("https://checkout.stripe.com/yearly")
|
||||
end
|
||||
|
||||
it "crea checkout per cambio piano con subscription esistente" do
|
||||
Billing::AssignPlan.call(
|
||||
club: club,
|
||||
plan_slug: "premium_light",
|
||||
status: "active",
|
||||
stripe_attrs: { stripe_customer_id: "cus_x", stripe_subscription_id: "sub_x" }
|
||||
)
|
||||
|
||||
session = double(url: "https://checkout.stripe.com/change")
|
||||
expect(Billing::Stripe::CustomerSync).to receive(:call).with(club: club, customer_id: "cus_x")
|
||||
it "crea checkout mensile" do
|
||||
session = double(url: "https://checkout.stripe.com/monthly")
|
||||
allow(Stripe::Customer).to receive(:create).and_return(double(id: "cus_m"))
|
||||
allow(Billing::Stripe::CustomerSync).to receive(:call)
|
||||
expect(Stripe::Checkout::Session).to receive(:create).with(
|
||||
hash_including(
|
||||
customer: "cus_x",
|
||||
subscription: "sub_x",
|
||||
line_items: [{ price: "price_full", quantity: 1 }]
|
||||
)
|
||||
hash_including(line_items: [{ price: "price_light_m", quantity: 1 }])
|
||||
).and_return(session)
|
||||
|
||||
url = described_class.new(club: club, user: user, plan_slug: "premium_full").url
|
||||
expect(url).to eq("https://checkout.stripe.com/change")
|
||||
url = described_class.new(club: club, user: user, plan_slug: "premium_light", interval: "monthly").url
|
||||
expect(url).to eq("https://checkout.stripe.com/monthly")
|
||||
end
|
||||
end
|
||||
|
||||
72
backend/spec/services/billing/stripe/checkout_sync_spec.rb
Normal file
72
backend/spec/services/billing/stripe/checkout_sync_spec.rb
Normal file
@@ -0,0 +1,72 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Billing::Stripe::CheckoutSync do
|
||||
let(:club) do
|
||||
Club.create!(
|
||||
name: "Sync Club", sport: "volleyball",
|
||||
primary_color: "#e53935", secondary_color: "#ffffff",
|
||||
billing_legal_name: "ASD", billing_email: "b@test.it",
|
||||
billing_address_line: "Via 1", billing_city: "Milano", billing_postal_code: "20100",
|
||||
billing_vat_number: "12345678901", billing_recipient_code: "ABCDEFG"
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
load Rails.root.join("db/seeds/plans.rb")
|
||||
Billing::AssignPlan.call(club: club, plan_slug: "free")
|
||||
club.subscription.update!(stripe_customer_id: "cus_sync")
|
||||
allow(MatchLiveTv).to receive(:stripe_enabled?).and_return(true)
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_monthly_price_id).and_return("price_light_m")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_yearly_price_id).and_return("price_light_y")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_monthly_price_id).and_return("price_full_m")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_yearly_price_id).and_return("price_full_y")
|
||||
end
|
||||
|
||||
it "allinea il piano da checkout session pagata" do
|
||||
session = double(
|
||||
customer: "cus_sync",
|
||||
subscription: "sub_new",
|
||||
payment_status: "paid",
|
||||
status: "complete",
|
||||
metadata: { "club_id" => club.id, "plan_slug" => "premium_light", "billing_interval" => "monthly" }
|
||||
)
|
||||
stripe_sub = double(
|
||||
id: "sub_new",
|
||||
customer: "cus_sync",
|
||||
status: "active",
|
||||
cancel_at_period_end: false,
|
||||
metadata: { "club_id" => club.id, "plan_slug" => "premium_light", "billing_interval" => "monthly" },
|
||||
items: double(data: [double(price: double(id: "price_light_m", recurring: double(interval: "month")))])
|
||||
)
|
||||
allow(Stripe::Checkout::Session).to receive(:retrieve).with("cs_test").and_return(session)
|
||||
allow(Stripe::Subscription).to receive(:retrieve).with("sub_new").and_return(stripe_sub)
|
||||
allow(Billing::Stripe::SubscriptionPeriod).to receive(:times).and_return([Time.current, 1.month.from_now])
|
||||
|
||||
described_class.call(club: club, session_id: "cs_test")
|
||||
|
||||
sub = club.reload.subscription
|
||||
expect(sub.plan.slug).to eq("premium_light")
|
||||
expect(sub.billing_interval).to eq("monthly")
|
||||
expect(sub.stripe_subscription_id).to eq("sub_new")
|
||||
end
|
||||
|
||||
it "usa l'abbonamento Stripe attivo del cliente se manca session_id" do
|
||||
stripe_sub = double(
|
||||
id: "sub_fallback",
|
||||
customer: "cus_sync",
|
||||
status: "active",
|
||||
created: 2,
|
||||
cancel_at_period_end: false,
|
||||
metadata: { "club_id" => club.id, "plan_slug" => "premium_light", "billing_interval" => "yearly" },
|
||||
items: double(data: [double(price: double(id: "price_light_y", recurring: double(interval: "year")))])
|
||||
)
|
||||
list = double(data: [stripe_sub])
|
||||
allow(Stripe::Subscription).to receive(:list).and_return(list)
|
||||
allow(Billing::Stripe::SubscriptionPeriod).to receive(:times).and_return([Time.current, 1.year.from_now])
|
||||
|
||||
described_class.call(club: club)
|
||||
|
||||
expect(club.reload.subscription.plan.slug).to eq("premium_light")
|
||||
expect(club.subscription.billing_interval).to eq("yearly")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Billing::Stripe::FinalizeSubscription do
|
||||
let(:club) do
|
||||
Club.create!(
|
||||
name: "Fin Club", sport: "volleyball",
|
||||
primary_color: "#e53935", secondary_color: "#ffffff"
|
||||
)
|
||||
end
|
||||
|
||||
before { load Rails.root.join("db/seeds/plans.rb") }
|
||||
|
||||
it "passa a free quando il periodo è scaduto e la disdetta era programmata" do
|
||||
Billing::AssignPlan.call(
|
||||
club: club,
|
||||
plan_slug: "premium_light",
|
||||
status: "active",
|
||||
stripe_attrs: {
|
||||
stripe_subscription_id: "sub_fin",
|
||||
cancel_at_period_end: true,
|
||||
current_period_end: 1.day.ago
|
||||
}
|
||||
)
|
||||
stripe_sub = double(
|
||||
id: "sub_fin",
|
||||
status: "active",
|
||||
cancel_at_period_end: true
|
||||
)
|
||||
allow(Stripe::Subscription).to receive(:retrieve).with("sub_fin").and_return(stripe_sub)
|
||||
allow(Billing::Stripe::SubscriptionPeriod).to receive(:times).and_return([2.months.ago, 1.day.ago])
|
||||
|
||||
expect(described_class.call(club: club)).to be true
|
||||
expect(club.reload.subscription.plan.slug).to eq("free")
|
||||
expect(club.subscription.stripe_subscription_id).to be_nil
|
||||
end
|
||||
|
||||
it "non cambia piano se il periodo non è ancora scaduto" do
|
||||
Billing::AssignPlan.call(
|
||||
club: club,
|
||||
plan_slug: "premium_light",
|
||||
status: "active",
|
||||
stripe_attrs: {
|
||||
stripe_subscription_id: "sub_ok",
|
||||
cancel_at_period_end: true,
|
||||
current_period_end: 1.month.from_now
|
||||
}
|
||||
)
|
||||
stripe_sub = double(id: "sub_ok", status: "active", cancel_at_period_end: true)
|
||||
allow(Stripe::Subscription).to receive(:retrieve).and_return(stripe_sub)
|
||||
|
||||
expect(described_class.call(club: club)).to be false
|
||||
expect(club.reload.subscription.plan.slug).to eq("premium_light")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Billing::Stripe::InvoicePaymentRefs do
|
||||
it "legge payment_intent dalla raccolta payments (API Basil+)" do
|
||||
pi = double(id: "pi_new")
|
||||
payment = double(payment_intent: pi)
|
||||
invoice = double(
|
||||
payments: double(data: [double(payment: payment)])
|
||||
)
|
||||
|
||||
expect(described_class.payment_intent_id(invoice)).to eq("pi_new")
|
||||
expect(described_class.charge_id(invoice)).to be_nil
|
||||
end
|
||||
|
||||
it "non accede a payment_intent top-level sull'invoice" do
|
||||
invoice = double(payments: double(data: []))
|
||||
expect(invoice).not_to receive(:payment_intent)
|
||||
expect(described_class.payment_intent_id(invoice)).to be_nil
|
||||
end
|
||||
end
|
||||
29
backend/spec/services/billing/stripe/price_catalog_spec.rb
Normal file
29
backend/spec/services/billing/stripe/price_catalog_spec.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Billing::Stripe::PriceCatalog do
|
||||
before do
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_yearly_price_id).and_return("price_light_y")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_monthly_price_id).and_return("price_light_m")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_yearly_price_id).and_return("price_full_y")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_full_monthly_price_id).and_return("price_full_m")
|
||||
end
|
||||
|
||||
it "risolve price id per piano e intervallo" do
|
||||
expect(described_class.price_id(plan_slug: "premium_light", interval: "yearly")).to eq("price_light_y")
|
||||
expect(described_class.price_id(plan_slug: "premium_light", interval: "monthly")).to eq("price_light_m")
|
||||
expect(described_class.price_id(plan_slug: "premium_full", interval: "monthly")).to eq("price_full_m")
|
||||
end
|
||||
|
||||
it "usa yearly come default" do
|
||||
expect(described_class.normalize_interval(nil)).to eq("yearly")
|
||||
expect(described_class.price_id(plan_slug: "premium_light", interval: nil)).to eq("price_light_y")
|
||||
end
|
||||
|
||||
it "rifiuta intervalli non validi" do
|
||||
expect { described_class.normalize_interval("weekly") }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "elenca intervalli configurati" do
|
||||
expect(described_class.available_intervals(plan_slug: "premium_light")).to eq(%w[monthly yearly])
|
||||
end
|
||||
end
|
||||
@@ -5,27 +5,32 @@ RSpec.describe Billing::Stripe::RecordPayment do
|
||||
|
||||
before do
|
||||
load Rails.root.join("db/seeds/plans.rb")
|
||||
allow(MatchLiveTv).to receive(:stripe_premium_light_yearly_price_id).and_return("price_light_y")
|
||||
Billing::AssignPlan.call(club: club, plan_slug: "premium_light", stripe_attrs: { stripe_subscription_id: "sub_x" })
|
||||
end
|
||||
|
||||
it "crea un pagamento da fattura Stripe" do
|
||||
invoice = double(
|
||||
id: "in_test_1",
|
||||
subscription: "sub_x",
|
||||
metadata: { "club_id" => club.id },
|
||||
metadata: { "club_id" => club.id, "plan_slug" => "premium_light", "billing_interval" => "yearly" },
|
||||
parent: nil,
|
||||
amount_paid: 4000,
|
||||
currency: "eur",
|
||||
status: "paid",
|
||||
description: "Premium Light",
|
||||
payment_intent: "pi_x",
|
||||
charge: "ch_x",
|
||||
invoice_pdf: "https://stripe.com/invoice.pdf",
|
||||
hosted_invoice_url: "https://stripe.com/hosted",
|
||||
status_transitions: double(paid_at: Time.current.to_i),
|
||||
lines: double(data: [double(description: "Premium Light annuale")]),
|
||||
subscription_details: nil
|
||||
lines: double(data: [
|
||||
double(
|
||||
description: "Premium Light annuale",
|
||||
metadata: { "plan_slug" => "premium_light", "billing_interval" => "yearly" },
|
||||
pricing: double(price_details: double(price: "price_light_y"))
|
||||
)
|
||||
])
|
||||
)
|
||||
allow(invoice).to receive(:metadata).and_return({ "club_id" => club.id })
|
||||
allow(invoice).to receive(:metadata).and_return({ "club_id" => club.id, "plan_slug" => "premium_light", "billing_interval" => "yearly" })
|
||||
allow(Stripe::Invoice).to receive(:retrieve).and_return(invoice)
|
||||
|
||||
payment = described_class.call(stripe_invoice: invoice)
|
||||
|
||||
@@ -33,5 +38,6 @@ RSpec.describe Billing::Stripe::RecordPayment do
|
||||
expect(payment.club).to eq(club)
|
||||
expect(payment.amount_cents).to eq(4000)
|
||||
expect(payment.stripe_invoice_id).to eq("in_test_1")
|
||||
expect(payment.description).to eq("Premium Light — €40/anno")
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user