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:
@@ -1,33 +1,90 @@
|
||||
module Admin
|
||||
class BillingInvoicesController < BaseController
|
||||
before_action :set_club
|
||||
before_action :set_invoice, only: %i[edit update]
|
||||
|
||||
def index
|
||||
@invoices = @club.billing_invoices.recent
|
||||
@payments = @club.billing_payments.recent.includes(:invoice)
|
||||
@invoices = @club.billing_invoices.recent.includes(:billing_payment, pdf_attachment: :blob)
|
||||
end
|
||||
|
||||
def new
|
||||
@invoice = @club.billing_invoices.build(issued_on: Date.current, currency: "eur", source: "aruba")
|
||||
@payment = @club.billing_payments.find_by(id: params[:billing_payment_id]) if params[:billing_payment_id].present?
|
||||
if @payment&.invoice.present?
|
||||
redirect_to edit_admin_club_billing_invoice_path(@club, @payment.invoice), alert: "Esiste già una fattura per questo pagamento."
|
||||
return
|
||||
end
|
||||
|
||||
@invoice = @club.billing_invoices.build(
|
||||
issued_on: Date.current,
|
||||
currency: "eur",
|
||||
source: "manual",
|
||||
status: "draft",
|
||||
billing_payment: @payment,
|
||||
amount_cents: @payment&.amount_cents,
|
||||
number: suggested_invoice_number
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
@invoice = @club.billing_invoices.build(invoice_params)
|
||||
@invoice.source = "aruba"
|
||||
@invoice.source = "manual"
|
||||
@invoice.status = "draft"
|
||||
|
||||
if @invoice.save
|
||||
redirect_to admin_club_billing_invoices_path(@club), notice: "Fattura #{@invoice.number} registrata."
|
||||
redirect_to edit_admin_club_billing_invoice_path(@club, @invoice),
|
||||
notice: "Bozza fattura #{@invoice.number} creata. Carica il PDF e invia al cliente."
|
||||
else
|
||||
@payment = @invoice.billing_payment
|
||||
flash.now[:alert] = @invoice.errors.full_messages.join(", ")
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@payment = @invoice.billing_payment
|
||||
end
|
||||
|
||||
def update
|
||||
@invoice.assign_attributes(invoice_params.except(:pdf))
|
||||
|
||||
if issuing?
|
||||
Billing::IssueInvoice.call(invoice: @invoice, pdf: params.dig(:billing_invoice, :pdf))
|
||||
redirect_to admin_club_billing_invoices_path(@club),
|
||||
notice: "Fattura #{@invoice.number} emessa e inviata a #{@club.billing_email}."
|
||||
elsif @invoice.save
|
||||
redirect_to admin_club_billing_invoices_path(@club), notice: "Fattura #{@invoice.number} aggiornata."
|
||||
else
|
||||
@payment = @invoice.billing_payment
|
||||
flash.now[:alert] = @invoice.errors.full_messages.join(", ")
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
rescue Billing::IssueInvoice::Error => e
|
||||
@payment = @invoice.billing_payment
|
||||
flash.now[:alert] = e.message
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_club
|
||||
@club = Club.find(params[:club_id])
|
||||
end
|
||||
|
||||
def set_invoice
|
||||
@invoice = @club.billing_invoices.find(params[:id])
|
||||
end
|
||||
|
||||
def issuing?
|
||||
params[:commit].to_s == "Emetti e invia via email"
|
||||
end
|
||||
|
||||
def suggested_invoice_number
|
||||
year = Date.current.year
|
||||
count = @club.billing_invoices.where("number LIKE ?", "%#{year}%").count + 1
|
||||
"#{year}/#{count}"
|
||||
end
|
||||
|
||||
def invoice_params
|
||||
params.require(:billing_invoice).permit(
|
||||
:number,
|
||||
@@ -35,8 +92,6 @@ module Admin
|
||||
:amount_cents,
|
||||
:amount_euros,
|
||||
:currency,
|
||||
:status,
|
||||
:aruba_document_id,
|
||||
:billing_payment_id,
|
||||
:notes,
|
||||
:pdf
|
||||
|
||||
@@ -10,21 +10,34 @@ module Public
|
||||
def update_profile
|
||||
@club.assign_attributes(billing_profile_params)
|
||||
if @club.save(context: :billing_profile)
|
||||
redirect_to public_club_billing_path(@club), notice: "Dati di fatturazione aggiornati."
|
||||
if premium_checkout_return_params.present? && @club.billing_profile_complete?
|
||||
redirect_to public_club_checkout_path(
|
||||
@club,
|
||||
plan: premium_checkout_return_params[:plan],
|
||||
interval: premium_checkout_return_params[:interval]
|
||||
), notice: "Dati salvati. Procedi con il pagamento."
|
||||
else
|
||||
redirect_to public_club_billing_path(@club), notice: "Dati di fatturazione aggiornati."
|
||||
end
|
||||
else
|
||||
flash.now[:alert] = @club.errors.full_messages.join(", ")
|
||||
render :profile, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def sync_payments
|
||||
def cancel_subscription
|
||||
unless MatchLiveTv.stripe_enabled?
|
||||
redirect_to public_club_billing_path(@club), alert: "Stripe non configurato."
|
||||
return
|
||||
end
|
||||
|
||||
count = Billing::Stripe::SyncPayments.call(club: @club)
|
||||
redirect_to public_club_billing_path(@club), notice: "Sincronizzati #{count} pagamenti da Stripe."
|
||||
Billing::Stripe::CancelSubscription.call(club: @club)
|
||||
sub = @club.subscription.reload
|
||||
date = sub.current_period_end ? helpers.l_local(sub.current_period_end.to_date) : "fine periodo"
|
||||
redirect_to public_club_billing_path(@club),
|
||||
notice: "Abbonamento disdetto. Resta attivo fino al #{date}; da quel giorno passerai al piano Free."
|
||||
rescue ArgumentError, RuntimeError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ::Stripe::StripeError => e
|
||||
redirect_to public_club_billing_path(@club), alert: "Errore Stripe: #{e.message}"
|
||||
end
|
||||
@@ -49,6 +62,16 @@ module Public
|
||||
require_club_owner!(@club)
|
||||
end
|
||||
|
||||
def premium_checkout_return_params
|
||||
plan = params[:plan].presence_in(%w[premium_light premium_full])
|
||||
return nil if plan.blank?
|
||||
|
||||
{
|
||||
plan: plan,
|
||||
interval: params[:interval].presence || Billing::Stripe::PriceCatalog::DEFAULT_INTERVAL
|
||||
}
|
||||
end
|
||||
|
||||
def billing_profile_params
|
||||
params.require(:club).permit(
|
||||
:billing_entity_type,
|
||||
|
||||
@@ -32,7 +32,14 @@ module Public
|
||||
Billing::AssignPlan.call(club: club, plan_slug: plan)
|
||||
|
||||
if plan.in?(%w[premium_light premium_full]) && MatchLiveTv.stripe_enabled?
|
||||
redirect_to public_club_checkout_path(club, plan: plan)
|
||||
unless club.billing_profile_complete?
|
||||
redirect_to public_club_billing_profile_path(club, plan: plan, interval: checkout_interval_param),
|
||||
alert: "Completa i dati di fatturazione prima di attivare un piano premium."
|
||||
return
|
||||
end
|
||||
|
||||
interval = checkout_interval_param
|
||||
redirect_to public_club_checkout_path(club, plan: plan, interval: interval)
|
||||
else
|
||||
redirect_to public_club_path(club), notice: "Società e prima squadra create."
|
||||
end
|
||||
@@ -66,13 +73,14 @@ module Public
|
||||
|
||||
def billing
|
||||
require_club_owner!(@club)
|
||||
sync_checkout_return!
|
||||
finalize_subscription_if_due!
|
||||
@subscription = @club.subscription&.reload
|
||||
apply_checkout_flash!
|
||||
@team = @club.teams.first!
|
||||
@entitlements = @team.entitlements
|
||||
@subscription = @club.subscription
|
||||
@plans = Plan.ordered
|
||||
@payments = @club.billing_payments.recent.limit(50)
|
||||
@invoices = @club.billing_invoices.recent.limit(50)
|
||||
@payments = @club.billing_payments.recent.includes(:invoice).limit(50)
|
||||
end
|
||||
|
||||
def checkout
|
||||
@@ -83,15 +91,29 @@ module Public
|
||||
end
|
||||
|
||||
plan_slug = params[:plan].presence_in(%w[premium_light premium_full]) || "premium_light"
|
||||
interval = checkout_interval_param
|
||||
target_plan = Plan[plan_slug]
|
||||
sub = @club.subscription
|
||||
if sub&.stripe_subscription_id.present? && sub.active? && sub.plan.slug == plan_slug
|
||||
redirect_to public_club_billing_path(@club), notice: "Sei già su questo piano."
|
||||
|
||||
if sub&.stripe_subscription_id.present? && sub.active? &&
|
||||
sub.plan.slug == plan_slug && sub.billing_interval == interval
|
||||
redirect_to public_club_billing_path(@club),
|
||||
notice: "Sei già su #{target_plan.name} (#{Billing::Stripe::PriceCatalog.label(plan_slug: plan_slug, interval: interval)})."
|
||||
return
|
||||
end
|
||||
|
||||
# Sempre Stripe Checkout (hosted): i dati carta non passano mai dal nostro server.
|
||||
url = Billing::Stripe::CheckoutSession.new(club: @club, user: current_user, plan_slug: plan_slug).url
|
||||
redirect_to url, allow_other_host: true
|
||||
if sub&.stripe_subscription_id.present? && sub.active?
|
||||
Billing::Stripe::ChangePlan.call(club: @club, plan_slug: plan_slug, interval: interval)
|
||||
redirect_to public_club_billing_path(@club),
|
||||
notice: "Piano aggiornato a #{target_plan.name} (#{Billing::Stripe::PriceCatalog.label(plan_slug: plan_slug, interval: interval)}). Eventuale differenza gestita da Stripe."
|
||||
else
|
||||
url = Billing::Stripe::CheckoutSession.new(
|
||||
club: @club, user: current_user, plan_slug: plan_slug, interval: interval
|
||||
).url
|
||||
redirect_to url, allow_other_host: true
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ::Stripe::StripeError => e
|
||||
Rails.logger.warn("[Stripe checkout] #{e.message}")
|
||||
redirect_to public_club_billing_path(@club), alert: "Errore Stripe: #{e.message}"
|
||||
@@ -132,14 +154,44 @@ module Public
|
||||
return if plan_slug.blank?
|
||||
return if @club.billing_profile_complete?
|
||||
|
||||
redirect_to public_club_billing_profile_path(@club),
|
||||
alert: "Completa i dati di fatturazione prima di attivare un piano premium."
|
||||
redirect_to public_club_billing_profile_path(
|
||||
@club,
|
||||
plan: plan_slug,
|
||||
interval: params[:interval].presence
|
||||
), alert: "Completa i dati di fatturazione prima di attivare un piano premium."
|
||||
end
|
||||
|
||||
def checkout_interval_param
|
||||
Billing::Stripe::PriceCatalog.normalize_interval(params[:interval])
|
||||
rescue ArgumentError
|
||||
Billing::Stripe::PriceCatalog::DEFAULT_INTERVAL
|
||||
end
|
||||
|
||||
def finalize_subscription_if_due!
|
||||
return unless MatchLiveTv.stripe_enabled?
|
||||
|
||||
Billing::Stripe::FinalizeSubscription.call(club: @club)
|
||||
rescue ::Stripe::StripeError => e
|
||||
Rails.logger.warn("[Stripe finalize subscription] #{e.message}")
|
||||
end
|
||||
|
||||
def sync_checkout_return!
|
||||
return unless params[:checkout] == "success"
|
||||
return unless MatchLiveTv.stripe_enabled?
|
||||
|
||||
Billing::Stripe::CheckoutSync.call(
|
||||
club: @club,
|
||||
session_id: params[:session_id].presence
|
||||
)
|
||||
rescue ::Stripe::StripeError => e
|
||||
Rails.logger.warn("[Stripe checkout sync] #{e.message}")
|
||||
end
|
||||
|
||||
def apply_checkout_flash!
|
||||
case params[:checkout]
|
||||
when "success"
|
||||
flash.now[:notice] = "Pagamento completato! Il piano premium è attivo per la società."
|
||||
plan_name = @subscription&.plan&.name || "premium"
|
||||
flash.now[:notice] = "Pagamento completato! Piano attivo: #{plan_name}."
|
||||
when "canceled"
|
||||
flash.now[:alert] = "Pagamento annullato. Nessun addebito è stato effettuato."
|
||||
end
|
||||
|
||||
@@ -4,10 +4,17 @@ module Public
|
||||
|
||||
include ::SeoHelper
|
||||
include ::LegalHelper
|
||||
helper ApplicationHelper
|
||||
helper_method :current_user, :logged_in?
|
||||
|
||||
before_action :set_site_locale
|
||||
|
||||
private
|
||||
|
||||
def set_site_locale
|
||||
I18n.locale = :it
|
||||
end
|
||||
|
||||
def current_user
|
||||
return @current_user if defined?(@current_user)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user