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

@@ -3,7 +3,7 @@ module Billing
self.table_name = "billing_invoices"
STATUSES = %w[draft issued sent cancelled].freeze
SOURCES = %w[aruba manual].freeze
SOURCES = %w[manual aruba].freeze
belongs_to :club
belongs_to :billing_payment, class_name: "Billing::Payment", optional: true
@@ -15,7 +15,8 @@ module Billing
validates :currency, presence: true
validates :status, inclusion: { in: STATUSES }
validates :source, inclusion: { in: SOURCES }
validate :pdf_present_when_issued
validates :billing_payment_id, uniqueness: true, allow_nil: true
validate :pdf_present_when_issued, on: :issue
scope :recent, -> { order(issued_on: :desc, created_at: :desc) }
@@ -28,16 +29,35 @@ module Billing
end
def downloadable?
pdf.attached?
pdf.attached? && status.in?(%w[issued sent])
end
def draft?
status == "draft"
end
def display_number
n = number.to_s.strip
cleaned = n.gsub(/\baruba[-_\s]*/i, "").strip
cleaned.presence || n
end
def display_status
{
"draft" => "In preparazione",
"issued" => "Emessa",
"sent" => "Inviata",
"cancelled" => "Annullata"
}[status] || status
end
private
def pdf_present_when_issued
return unless status.in?(%w[issued sent])
return unless validation_context == :issue
return if pdf.attached?
errors.add(:pdf, "è obbligatorio per le fatture emesse")
errors.add(:pdf, "è obbligatorio per emettere la fattura")
end
end
end

View File

@@ -21,5 +21,17 @@ module Billing
def formatted_amount
format("%.2f €", amount_euros)
end
def display_description
PaymentDescription.for_payment(self)
end
def display_status
{ "paid" => "Pagato", "failed" => "Non riuscito", "refunded" => "Rimborsato" }[status] || status
end
def invoice_for_display
invoice
end
end
end

View File

@@ -20,18 +20,34 @@ module ClubBillingProfile
billing_profile_errors.empty?
end
# Campi minimi per intestazione fattura e invio (email PDF + SDI/PEC).
def billing_profile_errors
errors = []
errors << "Tipo intestatario" if billing_entity_type.blank?
errors << "Ragione sociale o nome intestatario" if billing_legal_name.blank?
errors << "Email di fatturazione" if billing_email.blank?
errors << "Indirizzo" if billing_address_line.blank?
errors << "Città" if billing_city.blank?
errors << "Provincia (sigla 2 lettere)" if billing_province.blank? || billing_province.to_s.length != 2
errors << "CAP" if billing_postal_code.blank?
errors << "P.IVA o Codice Fiscale" if billing_vat_number.blank? && billing_fiscal_code.blank?
errors << "Codice destinatario SDI o PEC" if billing_recipient_code.blank? && billing_pec.blank?
errors << "Paese (ISO)" if billing_country.blank? || billing_country.to_s.length != 2
errors.concat(billing_tax_id_errors)
errors << "Codice destinatario SDI (7 caratteri) o PEC" if billing_recipient_code.blank? && billing_pec.blank?
errors << "Codice destinatario SDI (7 caratteri)" if billing_recipient_code.present? && billing_recipient_code.length != 7
errors
end
def billing_tax_id_errors
case billing_entity_type
when "company"
billing_vat_number.blank? ? ["Partita IVA"] : []
when "individual"
billing_fiscal_code.blank? ? ["Codice Fiscale"] : []
else
billing_vat_number.blank? && billing_fiscal_code.blank? ? ["P.IVA o Codice Fiscale"] : []
end
end
def billing_profile_summary
parts = [billing_legal_name.presence, billing_vat_number.presence && "P.IVA #{billing_vat_number}"]
parts.compact.join(" · ")