| <%= club.name %> |
<%= sub&.plan&.name || t("admin.common.free_plan") %> |
<%= club.teams.size %> |
+
+
+ <%= t("admin.clubs.billing_status.#{billing_status}") %>
+
+ |
<% if sub&.admin_comped? %>
<%= t("admin.common.yes") %>
diff --git a/backend/app/views/admin/clubs/show.html.erb b/backend/app/views/admin/clubs/show.html.erb
index d048d8e..47280e9 100644
--- a/backend/app/views/admin/clubs/show.html.erb
+++ b/backend/app/views/admin/clubs/show.html.erb
@@ -8,6 +8,8 @@
· <%= link_to t("admin.clubs.show.youtube_platform_link"), admin_youtube_platform_path %>
+<%= render "admin/clubs/billing_profile", club: @club %>
+
<%= render "admin/clubs/comped_form", club: @club, subscription: @subscription, return_to: admin_club_path(@club) %>
<%= render "admin/clubs/quote_form", club: @club, quote: @quote, return_to: admin_club_path(@club) %>
diff --git a/backend/app/views/layouts/admin.html.erb b/backend/app/views/layouts/admin.html.erb
index 81f05c4..865e86b 100644
--- a/backend/app/views/layouts/admin.html.erb
+++ b/backend/app/views/layouts/admin.html.erb
@@ -5,7 +5,7 @@
<%= csrf_meta_tags %>
-
+
<%= yield :head %>
<% if content_for?(:replay_archive_styles) %>
diff --git a/backend/config/locales/admin.de.yml b/backend/config/locales/admin.de.yml
index 157a101..ad6ec88 100644
--- a/backend/config/locales/admin.de.yml
+++ b/backend/config/locales/admin.de.yml
@@ -239,8 +239,10 @@ de:
club: Verein
plan: Plan
teams: Teams
+ billing_profile: Rechnungsdaten
comped: Kostenlos
stripe: Stripe
+ quote: Vereinbart
manage: Verwalten
invoices: Rechnungen
show:
@@ -254,6 +256,13 @@ de:
youtube_premium_full_not_connected: Premium Full — Vereinskanal nicht verbunden (nutzt Match Live TV)
no_channel_hint_html: "Ohne verbundenen Kanal nutzt die App den Match-Live-TV-Kanal. %{link}."
link_channel: Kanal verbinden (Vereinsseite)
+ profile_title: Kontaktdaten & Rechnungsdaten
+ owner_title: Kontoinhaber
+ owner_name: Name
+ owner_email: E-Mail
+ owner_none: Kein Inhaber zugeordnet.
+ billing_title: Rechnungsdaten
+ billing_none: Keine Rechnungsdaten hinterlegt.
teams_title: Teams
no_teams: Keine Teams registriert.
table:
@@ -265,6 +274,10 @@ de:
concurrency_lead: Dasselbe Konto hat versucht, eine weitere Direktübertragung zu starten, während bereits eine lief.
concurrency_none: Keine Versuche für diesen Verein erfasst.
concurrency_all: Alle Konto-Missbräuche ansehen
+ billing_status:
+ complete: Vollständig
+ incomplete: Unvollständig
+ absent: Fehlend
comped:
title: Kostenloses Abonnement
description: "Sponsor oder Aktion: Vergib Premium Light/Full ohne Stripe-Zahlung. Jederzeit widerrufbar."
diff --git a/backend/config/locales/admin.en.yml b/backend/config/locales/admin.en.yml
index 040891d..2794675 100644
--- a/backend/config/locales/admin.en.yml
+++ b/backend/config/locales/admin.en.yml
@@ -239,8 +239,10 @@ en:
club: Club
plan: Plan
teams: Teams
+ billing_profile: Billing data
comped: Comp
stripe: Stripe
+ quote: Quoted
manage: Manage
invoices: Invoices
show:
@@ -254,6 +256,13 @@ en:
youtube_premium_full_not_connected: Premium Full — club channel not connected (using Match Live TV)
no_channel_hint_html: "Without a connected channel, the app uses the Match Live TV channel. %{link}."
link_channel: Connect channel (club page)
+ profile_title: Contact & billing details
+ owner_title: Account owner
+ owner_name: Name
+ owner_email: Email
+ owner_none: No owner associated.
+ billing_title: Billing details
+ billing_none: No billing details provided.
teams_title: Teams
no_teams: No teams registered.
table:
@@ -265,6 +274,10 @@ en:
concurrency_lead: Same account tried to start another live while one was already running.
concurrency_none: No attempts recorded for this club.
concurrency_all: View all account abuse
+ billing_status:
+ complete: Complete
+ incomplete: Incomplete
+ absent: Missing
comped:
title: Complimentary subscription
description: "Sponsor or promotion: grant Premium Light/Full without a Stripe payment. Revocable at any time."
diff --git a/backend/config/locales/admin.es.yml b/backend/config/locales/admin.es.yml
index 2c9fad1..52d578f 100644
--- a/backend/config/locales/admin.es.yml
+++ b/backend/config/locales/admin.es.yml
@@ -239,8 +239,10 @@ es:
club: Club
plan: Plan
teams: Equipos
+ billing_profile: Datos fiscales
comped: Cortesía
stripe: Stripe
+ quote: Acordado
manage: Gestionar
invoices: Facturas
show:
@@ -254,6 +256,13 @@ es:
youtube_premium_full_not_connected: Premium Full — canal del club no conectado (usa Match Live TV)
no_channel_hint_html: "Sin un canal conectado, la app usa el canal de Match Live TV. %{link}."
link_channel: Conectar canal (página del club)
+ profile_title: Contacto y datos fiscales
+ owner_title: Titular de la cuenta
+ owner_name: Nombre
+ owner_email: Correo
+ owner_none: No hay titular asociado.
+ billing_title: Datos de facturación
+ billing_none: No hay datos de facturación.
teams_title: Equipos
no_teams: No hay equipos registrados.
table:
@@ -265,6 +274,10 @@ es:
concurrency_lead: La misma cuenta intentó iniciar otro directo mientras ya había uno en curso.
concurrency_none: No hay intentos registrados para este club.
concurrency_all: Ver todos los abusos de cuenta
+ billing_status:
+ complete: Completo
+ incomplete: Incompleto
+ absent: Ausentes
comped:
title: Suscripción de cortesía
description: "Patrocinador o promoción: concede Premium Light/Full sin pago en Stripe. Revocable en cualquier momento."
diff --git a/backend/config/locales/admin.fr.yml b/backend/config/locales/admin.fr.yml
index 5ca6df1..87a798b 100644
--- a/backend/config/locales/admin.fr.yml
+++ b/backend/config/locales/admin.fr.yml
@@ -239,8 +239,10 @@ fr:
club: Club
plan: Forfait
teams: Équipes
+ billing_profile: Données fiscales
comped: Offert
stripe: Stripe
+ quote: Convenu
manage: Gérer
invoices: Factures
show:
@@ -254,6 +256,13 @@ fr:
youtube_premium_full_not_connected: Premium Full — chaîne du club non connectée (utilise Match Live TV)
no_channel_hint_html: "Sans chaîne connectée, l'application utilise la chaîne Match Live TV. %{link}."
link_channel: Connecter la chaîne (page du club)
+ profile_title: Coordonnées et données fiscales
+ owner_title: Titulaire du compte
+ owner_name: Nom
+ owner_email: E-mail
+ owner_none: Aucun titulaire associé.
+ billing_title: Données de facturation
+ billing_none: Aucune donnée de facturation renseignée.
teams_title: Équipes
no_teams: Aucune équipe enregistrée.
table:
@@ -265,6 +274,10 @@ fr:
concurrency_lead: Le même compte a tenté de démarrer un autre direct alors qu’un était déjà en cours.
concurrency_none: Aucune tentative enregistrée pour ce club.
concurrency_all: Voir tous les abus de compte
+ billing_status:
+ complete: Complet
+ incomplete: Incomplet
+ absent: Absentes
comped:
title: Abonnement offert
description: "Sponsor ou promotion : accordez Premium Light/Full sans paiement Stripe. Révocable à tout moment."
diff --git a/backend/config/locales/admin.it.yml b/backend/config/locales/admin.it.yml
index cb7a3f4..507afe8 100644
--- a/backend/config/locales/admin.it.yml
+++ b/backend/config/locales/admin.it.yml
@@ -243,6 +243,7 @@ it:
club: Società
plan: Piano
teams: Squadre
+ billing_profile: Dati fiscali
comped: Omaggio
stripe: Stripe
quote: Concordato
@@ -259,6 +260,13 @@ it:
youtube_premium_full_not_connected: Premium Full — canale società non collegato (usa Match Live TV)
no_channel_hint_html: "Senza canale collegato, l’app usa il canale Match Live TV. %{link}."
link_channel: Collega canale (pagina società)
+ profile_title: Anagrafica e dati fiscali
+ owner_title: Titolare account
+ owner_name: Nome
+ owner_email: Email
+ owner_none: Nessun titolare associato.
+ billing_title: Dati di fatturazione
+ billing_none: Nessun dato di fatturazione compilato.
teams_title: Squadre
no_teams: Nessuna squadra registrata.
table:
@@ -270,6 +278,10 @@ it:
concurrency_lead: Stesso account che ha provato ad avviare un’altra diretta mentre ne era già in corso una.
concurrency_none: Nessun tentativo registrato per questa società.
concurrency_all: Vedi tutti gli abusi account
+ billing_status:
+ complete: Completo
+ incomplete: Incompleto
+ absent: Assenti
comped:
title: Abbonamento omaggio
description: "Sponsor o promozione: assegna Premium Light/Full senza pagamento Stripe. Revocabile in qualsiasi momento."
diff --git a/backend/public/admin.css b/backend/public/admin.css
index 81242ff..a826d77 100644
--- a/backend/public/admin.css
+++ b/backend/public/admin.css
@@ -622,6 +622,35 @@ body.admin-body {
word-break: break-word;
}
+.admin-billing-status {
+ display: inline-block;
+ font-size: 0.78rem;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ padding: 0.15rem 0.5rem;
+ border-radius: 999px;
+ border: 1px solid #3a3a45;
+ color: #ccc;
+}
+
+.admin-billing-status--complete {
+ border-color: #2e7d32;
+ color: #a5f0b8;
+ background: #1b3d1b;
+}
+
+.admin-billing-status--incomplete {
+ border-color: #f9a825;
+ color: #ffe082;
+ background: #3d3210;
+}
+
+.admin-billing-status--absent {
+ border-color: #555;
+ color: #999;
+ background: #1a1a22;
+}
+
.billing-upload-form {
display: flex;
flex-direction: column;
diff --git a/backend/spec/models/club_billing_profile_spec.rb b/backend/spec/models/club_billing_profile_spec.rb
index 5caf949..3859009 100644
--- a/backend/spec/models/club_billing_profile_spec.rb
+++ b/backend/spec/models/club_billing_profile_spec.rb
@@ -41,4 +41,15 @@ RSpec.describe ClubBillingProfile do
club.billing_fiscal_code = "RSSMRA80A01H501U"
expect(club.billing_profile_complete?).to be true
end
+
+ it "classifies admin billing status" do
+ expect(club.billing_profile_admin_status).to eq(:complete)
+
+ club.billing_recipient_code = nil
+ club.billing_pec = nil
+ expect(club.billing_profile_admin_status).to eq(:incomplete)
+
+ empty = Club.create!(name: "Empty", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff")
+ expect(empty.billing_profile_admin_status).to eq(:absent)
+ end
end
diff --git a/backend/spec/requests/admin/clubs_billing_profile_spec.rb b/backend/spec/requests/admin/clubs_billing_profile_spec.rb
new file mode 100644
index 0000000..686a2ec
--- /dev/null
+++ b/backend/spec/requests/admin/clubs_billing_profile_spec.rb
@@ -0,0 +1,95 @@
+require "rails_helper"
+
+RSpec.describe "Admin clubs billing profile", type: :request do
+ let!(:admin) { AdminAccount.create!(username: "ops-clubs-billing", password: "Password123") }
+
+ let!(:complete_club) do
+ Club.create!(
+ name: "Club Completo",
+ sport: "volleyball",
+ primary_color: "#e53935",
+ secondary_color: "#ffffff",
+ billing_entity_type: "company",
+ billing_legal_name: "ASD Completo",
+ billing_email: "fatture@completo.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"
+ )
+ end
+
+ let!(:incomplete_club) do
+ Club.create!(
+ name: "Club Incompleto",
+ sport: "volleyball",
+ primary_color: "#e53935",
+ secondary_color: "#ffffff",
+ billing_entity_type: "company",
+ billing_legal_name: "ASD Incompleto",
+ billing_vat_number: "10987654321"
+ )
+ end
+
+ let!(:absent_club) do
+ Club.create!(
+ name: "Club Assente",
+ sport: "volleyball",
+ primary_color: "#e53935",
+ secondary_color: "#ffffff"
+ )
+ end
+
+ let!(:owner) do
+ user = User.create!(email: "owner-completo@test.it", name: "Mario Rossi", password: "Password123", role: "coach")
+ ClubMembership.create!(user: user, club: complete_club, role: "owner")
+ user
+ end
+
+ before do
+ post admin_login_path, params: { username: "ops-clubs-billing", password: "Password123" }
+ end
+
+ it "mostra i dati fiscali e il titolare nella scheda società" do
+ get admin_club_path(complete_club)
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Anagrafica e dati fiscali")
+ expect(response.body).to include("Mario Rossi")
+ expect(response.body).to include("owner-completo@test.it")
+ expect(response.body).to include("ASD Completo")
+ expect(response.body).to include("12345678901")
+ expect(response.body).to include("ABCDEFG")
+ expect(response.body).to include("Completo")
+ end
+
+ it "mostra lo stato incompleto con errori mancanti" do
+ get admin_club_path(incomplete_club)
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Incompleto")
+ expect(response.body).to include("ASD Incompleto")
+ expect(response.body).to include("10987654321")
+ end
+
+ it "mostra lo stato assente quando non ci sono dati fiscali" do
+ get admin_club_path(absent_club)
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Assenti")
+ expect(response.body).to include("Nessun dato di fatturazione compilato.")
+ end
+
+ it "elenca lo stato del profilo fiscale nella lista società" do
+ get admin_clubs_path
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Dati fiscali")
+ expect(response.body).to include("Completo")
+ expect(response.body).to include("Incompleto")
+ expect(response.body).to include("Assenti")
+ end
+end
|