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:
2026-06-02 17:10:12 +02:00
parent 148402a97c
commit a5e781729c
78 changed files with 2457 additions and 424 deletions

View 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

View File

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

View 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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View 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

View File

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