Admin fatturazione: coda pagamenti senza PDF e upload diretto.
Lista globale dei pagamenti da fatturare con dati intestazione, caricamento PDF per riga che crea la fattura e invia email; rimuove il flusso bozza manuale. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,33 @@
|
|||||||
module Admin
|
module Admin
|
||||||
class BillingController < BaseController
|
class BillingController < BaseController
|
||||||
def index
|
def index
|
||||||
@clubs = Club.order(:name).includes(:billing_payments, :billing_invoices)
|
@pending_payments = pending_scope.recent
|
||||||
|
@completed_payments = Billing::Payment.with_invoice_pdf.includes(:club, :invoice).recent.limit(40)
|
||||||
|
@filter_club = Club.find_by(id: params[:club_id]) if params[:club_id].present?
|
||||||
|
@clubs = Club.order(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def attach_pdf
|
||||||
|
payment = Billing::Payment.find(params[:payment_id])
|
||||||
|
Billing::AttachPaymentInvoice.call(payment: payment, pdf: params[:pdf])
|
||||||
|
redirect_to admin_billing_path(billing_redirect_params(payment)),
|
||||||
|
notice: "Fattura caricata e inviata a #{payment.club.billing_email}."
|
||||||
|
rescue Billing::AttachPaymentInvoice::Error, Billing::IssueInvoice::Error => e
|
||||||
|
payment ||= Billing::Payment.find_by(id: params[:payment_id])
|
||||||
|
redirect_to admin_billing_path(payment ? billing_redirect_params(payment) : {}),
|
||||||
|
alert: e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def pending_scope
|
||||||
|
scope = Billing::Payment.awaiting_invoice_pdf.includes(:club, invoice: { pdf_attachment: :blob })
|
||||||
|
scope = scope.where(club_id: params[:club_id]) if params[:club_id].present?
|
||||||
|
scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def billing_redirect_params(payment)
|
||||||
|
{ club_id: payment.club_id, anchor: "payment-#{payment.id}" }.compact
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,26 +4,11 @@ module Admin
|
|||||||
before_action :set_invoice, only: %i[edit update]
|
before_action :set_invoice, only: %i[edit update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@payments = @club.billing_payments.recent.includes(:invoice)
|
redirect_to admin_billing_path(club_id: @club.id)
|
||||||
@invoices = @club.billing_invoices.recent.includes(:billing_payment, pdf_attachment: :blob)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@payment = @club.billing_payments.find_by(id: params[:billing_payment_id]) if params[:billing_payment_id].present?
|
redirect_to admin_billing_path(club_id: @club.id), alert: "Carica il PDF dalla lista «Pagamenti da fatturare»."
|
||||||
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
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -33,7 +18,7 @@ module Admin
|
|||||||
|
|
||||||
if @invoice.save
|
if @invoice.save
|
||||||
redirect_to edit_admin_club_billing_invoice_path(@club, @invoice),
|
redirect_to edit_admin_club_billing_invoice_path(@club, @invoice),
|
||||||
notice: "Bozza fattura #{@invoice.number} creata. Carica il PDF e invia al cliente."
|
notice: "Bozza creata. Carica il PDF dalla pagina Fatturazione o qui sotto."
|
||||||
else
|
else
|
||||||
@payment = @invoice.billing_payment
|
@payment = @invoice.billing_payment
|
||||||
flash.now[:alert] = @invoice.errors.full_messages.join(", ")
|
flash.now[:alert] = @invoice.errors.full_messages.join(", ")
|
||||||
@@ -50,10 +35,10 @@ module Admin
|
|||||||
|
|
||||||
if issuing?
|
if issuing?
|
||||||
Billing::IssueInvoice.call(invoice: @invoice, pdf: params.dig(:billing_invoice, :pdf))
|
Billing::IssueInvoice.call(invoice: @invoice, pdf: params.dig(:billing_invoice, :pdf))
|
||||||
redirect_to admin_club_billing_invoices_path(@club),
|
redirect_to admin_billing_path(club_id: @club.id),
|
||||||
notice: "Fattura #{@invoice.number} emessa e inviata a #{@club.billing_email}."
|
notice: "Fattura #{@invoice.number} emessa e inviata a #{@club.billing_email}."
|
||||||
elsif @invoice.save
|
elsif @invoice.save
|
||||||
redirect_to admin_club_billing_invoices_path(@club), notice: "Fattura #{@invoice.number} aggiornata."
|
redirect_to admin_billing_path(club_id: @club.id), notice: "Fattura #{@invoice.number} aggiornata."
|
||||||
else
|
else
|
||||||
@payment = @invoice.billing_payment
|
@payment = @invoice.billing_payment
|
||||||
flash.now[:alert] = @invoice.errors.full_messages.join(", ")
|
flash.now[:alert] = @invoice.errors.full_messages.join(", ")
|
||||||
@@ -79,12 +64,6 @@ module Admin
|
|||||||
params[:commit].to_s == "Emetti e invia via email"
|
params[:commit].to_s == "Emetti e invia via email"
|
||||||
end
|
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
|
def invoice_params
|
||||||
params.require(:billing_invoice).permit(
|
params.require(:billing_invoice).permit(
|
||||||
:number,
|
:number,
|
||||||
|
|||||||
@@ -14,6 +14,41 @@ module Billing
|
|||||||
|
|
||||||
scope :recent, -> { order(paid_at: :desc, created_at: :desc) }
|
scope :recent, -> { order(paid_at: :desc, created_at: :desc) }
|
||||||
|
|
||||||
|
# Pagamenti pagati senza PDF fattura caricato (da gestire in admin).
|
||||||
|
scope :awaiting_invoice_pdf, lambda {
|
||||||
|
where(status: "paid").where(
|
||||||
|
<<~SQL.squish,
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM billing_invoices bi
|
||||||
|
INNER JOIN active_storage_attachments asa
|
||||||
|
ON asa.record_type = 'Billing::Invoice'
|
||||||
|
AND asa.record_id = bi.id
|
||||||
|
AND asa.name = 'pdf'
|
||||||
|
WHERE bi.billing_payment_id = billing_payments.id
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
scope :with_invoice_pdf, lambda {
|
||||||
|
where(status: "paid").where(
|
||||||
|
<<~SQL.squish,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM billing_invoices bi
|
||||||
|
INNER JOIN active_storage_attachments asa
|
||||||
|
ON asa.record_type = 'Billing::Invoice'
|
||||||
|
AND asa.record_id = bi.id
|
||||||
|
AND asa.name = 'pdf'
|
||||||
|
WHERE bi.billing_payment_id = billing_payments.id
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def awaiting_invoice_pdf?
|
||||||
|
status == "paid" && !invoice&.pdf&.attached?
|
||||||
|
end
|
||||||
|
|
||||||
def amount_euros
|
def amount_euros
|
||||||
amount_cents / 100.0
|
amount_cents / 100.0
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,6 +53,22 @@ module ClubBillingProfile
|
|||||||
parts.compact.join(" · ")
|
parts.compact.join(" · ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Righe per intestazione fattura in admin (etichetta, valore).
|
||||||
|
def billing_profile_invoice_lines
|
||||||
|
lines = []
|
||||||
|
lines << ["Tipo", BILLING_ENTITY_TYPES[billing_entity_type]] if billing_entity_type.present?
|
||||||
|
lines << ["Intestatario", billing_legal_name]
|
||||||
|
lines << ["P.IVA", billing_vat_number] if billing_vat_number.present?
|
||||||
|
lines << ["Codice fiscale", billing_fiscal_code] if billing_fiscal_code.present?
|
||||||
|
lines << ["Email fatturazione", billing_email]
|
||||||
|
lines << ["Telefono", billing_phone] if billing_phone.present?
|
||||||
|
addr = [billing_address_line, billing_postal_code, billing_city, billing_province, billing_country].compact.join(", ")
|
||||||
|
lines << ["Indirizzo", addr] if addr.present?
|
||||||
|
lines << ["SDI", billing_recipient_code] if billing_recipient_code.present?
|
||||||
|
lines << ["PEC", billing_pec] if billing_pec.present?
|
||||||
|
lines
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def billing_profile_for_invoicing
|
def billing_profile_for_invoicing
|
||||||
|
|||||||
37
backend/app/services/billing/attach_payment_invoice.rb
Normal file
37
backend/app/services/billing/attach_payment_invoice.rb
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
module Billing
|
||||||
|
class AttachPaymentInvoice
|
||||||
|
class Error < StandardError; end
|
||||||
|
|
||||||
|
def self.call(payment:, pdf:)
|
||||||
|
new(payment: payment, pdf: pdf).call
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(payment:, pdf:)
|
||||||
|
@payment = payment
|
||||||
|
@pdf = pdf
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
raise Error, "Seleziona un file PDF" if @pdf.blank?
|
||||||
|
raise Error, "Pagamento non valido" unless @payment.status == "paid"
|
||||||
|
|
||||||
|
club = @payment.club
|
||||||
|
invoice = @payment.invoice || build_invoice!(club)
|
||||||
|
IssueInvoice.call(invoice: invoice, pdf: @pdf)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_invoice!(club)
|
||||||
|
club.billing_invoices.create!(
|
||||||
|
number: InvoiceNumber.next_for(club),
|
||||||
|
issued_on: @payment.paid_at&.to_date || Date.current,
|
||||||
|
amount_cents: @payment.amount_cents,
|
||||||
|
currency: @payment.currency.presence || "eur",
|
||||||
|
status: "draft",
|
||||||
|
source: "manual",
|
||||||
|
billing_payment: @payment
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
9
backend/app/services/billing/invoice_number.rb
Normal file
9
backend/app/services/billing/invoice_number.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module Billing
|
||||||
|
class InvoiceNumber
|
||||||
|
def self.next_for(club)
|
||||||
|
year = Date.current.year
|
||||||
|
count = club.billing_invoices.where("number LIKE ?", "%#{year}%").count + 1
|
||||||
|
"#{year}/#{count}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,31 +1,92 @@
|
|||||||
<h2>Pagamenti e fatture</h2>
|
<h2>Pagamenti da fatturare</h2>
|
||||||
<p style="color:#666;margin-bottom:16px">
|
<p style="color:#666;margin-bottom:16px">
|
||||||
Elenco società: pagamenti da Stripe e fatture PDF da emettere o inviare al cliente.
|
Pagamenti Stripe <strong>pagati</strong> senza PDF fattura. Genera il PDF nei tuoi sistemi, poi caricalo qui: viene associato al pagamento e inviato via email al cliente.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table class="admin-table">
|
<% if @clubs.many? %>
|
||||||
<thead>
|
<p style="margin-bottom:16px">
|
||||||
<tr>
|
Filtra società:
|
||||||
<th>Società</th>
|
<%= link_to "Tutte", admin_billing_path, class: (@filter_club ? nil : "admin-nav-active") %>
|
||||||
<th>Pagamenti</th>
|
|
||||||
<th>Fatture</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% @clubs.each do |club| %>
|
<% @clubs.each do |club| %>
|
||||||
<tr>
|
· <%= link_to club.name, admin_billing_path(club_id: club.id), class: (@filter_club&.id == club.id ? "admin-nav-active" : nil) %>
|
||||||
<td><strong><%= club.name %></strong></td>
|
|
||||||
<td><%= club.billing_payments.size %></td>
|
|
||||||
<td><%= club.billing_invoices.size %></td>
|
|
||||||
<td><%= link_to "Apri", admin_club_billing_invoices_path(club), class: "admin-btn admin-btn--sm" %></td>
|
|
||||||
</tr>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</p>
|
||||||
</table>
|
|
||||||
|
|
||||||
<% if @clubs.empty? %>
|
|
||||||
<p>Nessuna società registrata.</p>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<p style="margin-top:20px"><%= link_to "← Dashboard", admin_root_path %></p>
|
<% if @pending_payments.any? %>
|
||||||
|
<div class="billing-pending-list">
|
||||||
|
<% @pending_payments.each do |payment| %>
|
||||||
|
<% club = payment.club %>
|
||||||
|
<article id="payment-<%= payment.id %>" class="billing-pending-card">
|
||||||
|
<header class="billing-pending-card__head">
|
||||||
|
<div>
|
||||||
|
<strong><%= club.name %></strong>
|
||||||
|
· <%= (payment.paid_at || payment.created_at).to_date.strftime("%d/%m/%Y") %>
|
||||||
|
· <%= payment.display_description %>
|
||||||
|
· <strong><%= payment.formatted_amount %></strong>
|
||||||
|
</div>
|
||||||
|
<% unless club.billing_profile_complete? %>
|
||||||
|
<span class="billing-pending-card__warn">Dati fatturazione incompleti</span>
|
||||||
|
<% end %>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="billing-pending-card__body">
|
||||||
|
<% if club.billing_profile_invoice_lines.any? %>
|
||||||
|
<dl class="billing-profile-dl">
|
||||||
|
<% club.billing_profile_invoice_lines.each do |label, value| %>
|
||||||
|
<dt><%= label %></dt>
|
||||||
|
<dd><%= value %></dd>
|
||||||
|
<% end %>
|
||||||
|
</dl>
|
||||||
|
<% else %>
|
||||||
|
<p class="billing-pending-card__warn">Nessun dato di fatturazione — il cliente deve completare il profilo.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= form_with url: admin_billing_payment_attach_pdf_path(payment),
|
||||||
|
method: :post, multipart: true, local: true, class: "billing-upload-form" do %>
|
||||||
|
<label class="billing-upload-form__label">
|
||||||
|
PDF fattura
|
||||||
|
<%= file_field_tag :pdf, accept: "application/pdf", required: true %>
|
||||||
|
</label>
|
||||||
|
<%= submit_tag "Carica PDF e invia al cliente", class: "admin-btn admin-btn--primary" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="admin-flash" style="background:#1b3d1b;border-color:#2e7d32">
|
||||||
|
Nessun pagamento in attesa di fattura<%= @filter_club ? " per #{@filter_club.name}" : "" %>.
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @completed_payments.any? %>
|
||||||
|
<h2 style="margin-top:40px;font-size:1.15rem">Fatture già caricate</h2>
|
||||||
|
<table class="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Società</th>
|
||||||
|
<th>Descrizione</th>
|
||||||
|
<th>Importo</th>
|
||||||
|
<th>Fattura</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @completed_payments.each do |payment| %>
|
||||||
|
<% inv = payment.invoice %>
|
||||||
|
<tr>
|
||||||
|
<td><%= payment.paid_at&.to_date || payment.created_at.to_date %></td>
|
||||||
|
<td><%= payment.club.name %></td>
|
||||||
|
<td><%= payment.display_description %></td>
|
||||||
|
<td><%= payment.formatted_amount %></td>
|
||||||
|
<td><%= inv&.display_number %></td>
|
||||||
|
<td><%= inv&.display_status %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p style="margin-top:24px"><%= link_to "← Dashboard", admin_root_path %></p>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<td><%= t.youtube_credential.present? ? "✓" : "—" %></td>
|
<td><%= t.youtube_credential.present? ? "✓" : "—" %></td>
|
||||||
<td>
|
<td>
|
||||||
<% if t.club %>
|
<% if t.club %>
|
||||||
<%= link_to "Pagamenti", admin_club_billing_invoices_path(t.club) %>
|
<%= link_to "Fatturazione", admin_billing_path(club_id: t.club.id) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
—
|
—
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<h2><%= @team.name %></h2>
|
<h2><%= @team.name %></h2>
|
||||||
<p>Sport: <%= @team.sport %></p>
|
<p>Sport: <%= @team.sport %></p>
|
||||||
<% if @team.club %>
|
<% if @team.club %>
|
||||||
<p>Società: <%= @team.club.name %> · <%= link_to "Fatture società", admin_club_billing_invoices_path(@team.club) %></p>
|
<p>Società: <%= @team.club.name %> · <%= link_to "Pagamenti e fatture", admin_billing_path(club_id: @team.club.id) %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<p>YouTube: <%= @team.youtube_credential ? "Connesso" : link_to("Connetti", "/api/v1/teams/#{@team.id}/youtube/authorize") %></p>
|
<p>YouTube: <%= @team.youtube_credential ? "Connesso" : link_to("Connetti", "/api/v1/teams/#{@team.id}/youtube/authorize") %></p>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<title>Match Live TV Admin</title>
|
<title>Match Live TV Admin</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
<link rel="stylesheet" href="/admin.css?v=1">
|
<link rel="stylesheet" href="/admin.css?v=2">
|
||||||
<% if controller_name == "dashboard" %>
|
<% if controller_name == "dashboard" %>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script>
|
||||||
<script src="/admin-dashboard.js?v=1" defer></script>
|
<script src="/admin-dashboard.js?v=1" defer></script>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ Rails.application.routes.draw do
|
|||||||
root to: "dashboard#index"
|
root to: "dashboard#index"
|
||||||
get "metrics", to: "dashboard#metrics"
|
get "metrics", to: "dashboard#metrics"
|
||||||
get "billing", to: "billing#index", as: :billing
|
get "billing", to: "billing#index", as: :billing
|
||||||
|
post "billing/payments/:payment_id/attach_pdf", to: "billing#attach_pdf", as: :billing_payment_attach_pdf
|
||||||
resources :teams, only: %i[index show]
|
resources :teams, only: %i[index show]
|
||||||
resources :clubs, only: [] do
|
resources :clubs, only: [] do
|
||||||
resources :billing_invoices, only: %i[index new create edit update], controller: "billing_invoices"
|
resources :billing_invoices, only: %i[index new create edit update], controller: "billing_invoices"
|
||||||
|
|||||||
@@ -285,3 +285,86 @@ body.admin-body {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.billing-pending-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-pending-card {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-pending-card__head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem 1rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--card-border);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-pending-card__warn {
|
||||||
|
color: #ffb74d;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-pending-card__body {
|
||||||
|
padding: 1rem 1.25rem 1.25rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.billing-pending-card__body {
|
||||||
|
grid-template-columns: 1fr minmax(220px, 280px);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-profile-dl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 0.35rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-profile-dl dt {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-profile-dl dd {
|
||||||
|
margin: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-upload-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-upload-form__label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-upload-form input[type="file"] {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav-active {
|
||||||
|
color: var(--red) !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
40
backend/spec/services/billing/attach_payment_invoice_spec.rb
Normal file
40
backend/spec/services/billing/attach_payment_invoice_spec.rb
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe Billing::AttachPaymentInvoice do
|
||||||
|
let!(:club) do
|
||||||
|
Club.create!(
|
||||||
|
name: "Attach Club", sport: "volleyball",
|
||||||
|
primary_color: "#e53935", secondary_color: "#ffffff",
|
||||||
|
billing_entity_type: "company", billing_legal_name: "ASD Attach",
|
||||||
|
billing_email: "fatture@attach.test", 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"
|
||||||
|
)
|
||||||
|
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(:pdf) do
|
||||||
|
{ io: StringIO.new("%PDF-1.4 test"), filename: "fattura.pdf", content_type: "application/pdf" }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "crea fattura, allega PDF e invia email" do
|
||||||
|
mail = instance_double(ActionMailer::MessageDelivery, deliver_now: true)
|
||||||
|
allow(Billing::InvoiceMailer).to receive(:with).and_return(
|
||||||
|
instance_double(Billing::InvoiceMailer, invoice_pdf: mail)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.call(payment: payment, pdf: pdf)
|
||||||
|
}.to change { club.billing_invoices.count }.by(1)
|
||||||
|
|
||||||
|
inv = payment.reload.invoice
|
||||||
|
expect(inv.pdf).to be_attached
|
||||||
|
expect(inv.status).to eq("sent")
|
||||||
|
expect(Billing::InvoiceMailer).to have_received(:with).with(invoice: inv)
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user