diff --git a/backend/app/controllers/public/clubs_controller.rb b/backend/app/controllers/public/clubs_controller.rb index 4da8036..895d579 100644 --- a/backend/app/controllers/public/clubs_controller.rb +++ b/backend/app/controllers/public/clubs_controller.rb @@ -23,23 +23,28 @@ module Public ClubMembership.create!(user: current_user, club: club, role: "owner") first_team_name = params.dig(:first_team, :name).presence || t("club.new.default_first_team_name") - club.teams.create!( + first_team = club.teams.create!( name: first_team_name, sport: club.sport ) - plan = params[:plan].presence_in(%w[free premium_light premium_full]) || "free" - Billing::AssignPlan.call(club: club, plan_slug: plan) + desired_plan = params[:plan].presence_in(%w[free premium_light premium_full]) || "free" + Billing::AssignPlan.call(club: club, plan_slug: "free") + Teams::StaffAssignment.designate_club_owner!(team: first_team, user: current_user) - if plan.in?(%w[premium_light premium_full]) && MatchLiveTv.stripe_enabled? - unless club.billing_profile_complete? - redirect_to public_club_billing_profile_path(club, plan: plan, interval: checkout_interval_param), - alert: t("flash.clubs.complete_billing_first") - return + if desired_plan.in?(%w[premium_light premium_full]) + interval = Billing::Stripe::PriceCatalog::DEFAULT_INTERVAL + if MatchLiveTv.stripe_enabled? + unless club.billing_profile_complete? + redirect_to public_club_billing_profile_path(club, plan: desired_plan, interval: interval), + alert: t("flash.clubs.complete_billing_first") + return + end + + redirect_to public_club_checkout_path(club, plan: desired_plan, interval: interval) + else + redirect_to public_club_billing_path(club), notice: t("flash.clubs.created_complete_subscription") end - - interval = checkout_interval_param - redirect_to public_club_checkout_path(club, plan: plan, interval: interval) else redirect_to public_club_path(club), notice: t("flash.clubs.created") end diff --git a/backend/app/controllers/public/teams_controller.rb b/backend/app/controllers/public/teams_controller.rb index 84e6374..22ef10b 100644 --- a/backend/app/controllers/public/teams_controller.rb +++ b/backend/app/controllers/public/teams_controller.rb @@ -19,6 +19,7 @@ module Public require_club_owner!(@club) team = @club.teams.create!(team_params) attach_branding_logo(team) + Teams::StaffAssignment.designate_club_owner!(team: team, user: current_user) redirect_to public_club_path(@club), notice: t("flash.teams.added", name: team.name) rescue ActiveRecord::RecordInvalid => e flash.now[:alert] = e.record.errors.full_messages.join(", ") diff --git a/backend/app/helpers/public/billing_helper.rb b/backend/app/helpers/public/billing_helper.rb index 63ec21e..152ebe4 100644 --- a/backend/app/helpers/public/billing_helper.rb +++ b/backend/app/helpers/public/billing_helper.rb @@ -11,6 +11,13 @@ module Public quote = club&.active_billing_quote if quote if quote.plan_slug == target_plan.slug + if current_paid_plan?(subscription, target_plan) + return { + kind: :current, + label: "#{I18n.t('billing.actions.current_plan')} — #{quote.price_label}" + } + end + return { kind: :quoted, plan: target_plan, quote: quote, intervals: [quote.billing_interval] } end @@ -19,6 +26,9 @@ module Public intervals = bank_transfer_intervals_for(target_plan) unless MatchLiveTv.stripe_enabled? + if current_paid_plan?(subscription, target_plan) + return { kind: :current, label: current_plan_label(target_plan, subscription) } + end if MatchLiveTv.bank_transfer_configured? return { kind: :bank_only, plan: target_plan, intervals: intervals } end @@ -28,6 +38,9 @@ module Public stripe_intervals = Billing::Stripe::PriceCatalog.available_intervals(plan_slug: target_plan.slug) if stripe_intervals.empty? + if current_paid_plan?(subscription, target_plan) + return { kind: :current, label: current_plan_label(target_plan, subscription) } + end if MatchLiveTv.bank_transfer_configured? return { kind: :bank_only, plan: target_plan, intervals: intervals } end @@ -54,6 +67,10 @@ module Public } end + if current_paid_plan?(subscription, target_plan) + return { kind: :current, label: current_plan_label(target_plan, subscription) } + end + if current_slug == "free" || !stripe_subscription_active { kind: :checkout_options, plan: target_plan, intervals: stripe_intervals.presence || intervals, subscription: subscription } else @@ -136,8 +153,21 @@ module Public return false unless MatchLiveTv.bank_transfer_configured? return false if quote && !quote.matches?(plan.slug, interval) return false if pending_transfer&.awaiting_payment? + return false if current_paid_plan?(club.subscription, plan) true end + + def current_paid_plan?(subscription, target_plan) + return false unless subscription&.active? && subscription.premium? + return false if subscription.plan_change_pending? + + subscription.plan.slug == target_plan.slug + end + + def current_plan_label(target_plan, subscription) + interval = subscription&.billing_interval.presence || Billing::Stripe::PriceCatalog::DEFAULT_INTERVAL + "#{I18n.t('billing.actions.current_plan')} — #{Billing::Stripe::PriceCatalog.label(plan_slug: target_plan.slug, interval: interval)}" + end end end diff --git a/backend/app/services/billing/confirm_bank_transfer.rb b/backend/app/services/billing/confirm_bank_transfer.rb index 2c28475..c228981 100644 --- a/backend/app/services/billing/confirm_bank_transfer.rb +++ b/backend/app/services/billing/confirm_bank_transfer.rb @@ -88,7 +88,7 @@ module Billing mailer_action: :plan_activated_with_invoice ) else - BankTransferMailer.with(order: @order.reload).plan_activated.deliver_now + MatchLiveTv.deliver_mail(BankTransferMailer.with(order: @order.reload).plan_activated) end end diff --git a/backend/app/services/billing/issue_invoice.rb b/backend/app/services/billing/issue_invoice.rb index bdabfe2..ae25159 100644 --- a/backend/app/services/billing/issue_invoice.rb +++ b/backend/app/services/billing/issue_invoice.rb @@ -26,8 +26,10 @@ module Billing @invoice.update!(status: "issued") - Billing::InvoiceMailer.with(invoice: @invoice).public_send(@mailer_action).deliver_now - @invoice.update!(status: "sent", emailed_at: Time.current) + mail = Billing::InvoiceMailer.with(invoice: @invoice).public_send(@mailer_action) + if MatchLiveTv.deliver_mail(mail) + @invoice.update!(status: "sent", emailed_at: Time.current) + end @invoice end diff --git a/backend/app/services/billing/request_bank_transfer.rb b/backend/app/services/billing/request_bank_transfer.rb index 92197ef..c95deca 100644 --- a/backend/app/services/billing/request_bank_transfer.rb +++ b/backend/app/services/billing/request_bank_transfer.rb @@ -28,7 +28,7 @@ module Billing existing = @club.billing_transfer_orders.awaiting_payment.first if existing if existing.plan_slug == @plan_slug && existing.billing_interval == @interval && existing.amount_cents == amount_cents - BankTransferMailer.with(order: existing).instructions.deliver_now + MatchLiveTv.deliver_mail(BankTransferMailer.with(order: existing).instructions) return existing end @@ -59,7 +59,7 @@ module Billing ) end - BankTransferMailer.with(order: order).instructions.deliver_now + MatchLiveTv.deliver_mail(BankTransferMailer.with(order: order).instructions) order end diff --git a/backend/app/services/teams/staff_assignment.rb b/backend/app/services/teams/staff_assignment.rb index 14ecc2e..330b2ae 100644 --- a/backend/app/services/teams/staff_assignment.rb +++ b/backend/app/services/teams/staff_assignment.rb @@ -23,5 +23,12 @@ module Teams ut.update!(staff_kind: "transmission") ut end + + def self.designate_club_owner!(team:, user:) + membership = user.user_teams.find_or_initialize_by(team: team) + membership.role = "member" if membership.new_record? + membership.save! + call(team: team, user: user, membership: membership) + end end end diff --git a/backend/app/views/admin/billing/index.html.erb b/backend/app/views/admin/billing/index.html.erb index 824c2d1..4194e4c 100644 --- a/backend/app/views/admin/billing/index.html.erb +++ b/backend/app/views/admin/billing/index.html.erb @@ -42,6 +42,10 @@

+ <%= t("admin.billing.index.transfers_table.holder") %>: + <%= MatchLiveTv.bank_transfer_account_holder %>
+ <%= t("admin.billing.index.transfers_table.iban") %>: + <%= MatchLiveTv.bank_transfer_iban %>
<%= t("admin.billing.index.transfers_table.causal") %>: <%= order.payment_causal %>

diff --git a/backend/app/views/layouts/admin.html.erb b/backend/app/views/layouts/admin.html.erb index 1d1369f..9506970 100644 --- a/backend/app/views/layouts/admin.html.erb +++ b/backend/app/views/layouts/admin.html.erb @@ -13,7 +13,7 @@ <% end %> - +
diff --git a/backend/app/views/layouts/marketing.html.erb b/backend/app/views/layouts/marketing.html.erb index ff5b72a..810a0ea 100644 --- a/backend/app/views/layouts/marketing.html.erb +++ b/backend/app/views/layouts/marketing.html.erb @@ -23,7 +23,7 @@ - + diff --git a/backend/app/views/layouts/marketing_live.html.erb b/backend/app/views/layouts/marketing_live.html.erb index e4cb00c..0c7d033 100644 --- a/backend/app/views/layouts/marketing_live.html.erb +++ b/backend/app/views/layouts/marketing_live.html.erb @@ -18,7 +18,7 @@ <%= render "shared/marketing_footer" %> - + diff --git a/backend/app/views/public/clubs/billing.html.erb b/backend/app/views/public/clubs/billing.html.erb index 5733080..fd99db2 100644 --- a/backend/app/views/public/clubs/billing.html.erb +++ b/backend/app/views/public/clubs/billing.html.erb @@ -18,7 +18,7 @@
<% else %> <% if @quote %> - <%= render "shared/quoted_price_banner", quote: @quote %> + <%= render "shared/quoted_price_banner", quote: @quote, subscription: @subscription %> <% elsif MatchLiveTv.stripe_enabled? %> <%= render "shared/stripe_secure_payment" %> <% end %> diff --git a/backend/app/views/public/clubs/new.html.erb b/backend/app/views/public/clubs/new.html.erb index 8efe7cd..a392dc6 100644 --- a/backend/app/views/public/clubs/new.html.erb +++ b/backend/app/views/public/clubs/new.html.erb @@ -24,15 +24,8 @@ [t("club.new.plan_free"), "free"], [t("club.new.plan_light"), "premium_light"], [t("club.new.plan_full"), "premium_full"] - ], params[:plan] || "free"), id: "club_plan_select" %> -
- <%= label_tag :interval, t("club.new.interval_label") %> - <%= select_tag :interval, options_for_select([ - [t("club.new.interval_yearly"), "yearly"], - [t("club.new.interval_monthly"), "monthly"] - ], params[:interval] || "yearly") %> -
- <%= render "shared/stripe_secure_payment", compact: true %> + ], params[:plan] || "free") %> +

<%= t("club.new.plan_hint") %>

<%= submit_tag t("club.new.submit"), class: "btn btn-primary" %> <% end %> diff --git a/backend/app/views/public/teams/_streaming_staff.html.erb b/backend/app/views/public/teams/_streaming_staff.html.erb index e367be4..0af7fa3 100644 --- a/backend/app/views/public/teams/_streaming_staff.html.erb +++ b/backend/app/views/public/teams/_streaming_staff.html.erb @@ -1,33 +1,33 @@ <% team = local_assigns[:team] %> +<% club = local_assigns[:club] %> <% can_manage = local_assigns[:can_manage] %> -<% owner_membership = local_assigns[:owner_membership] %> <% staff_memberships = local_assigns[:staff_memberships] %> <% pending_invitations = local_assigns[:pending_invitations] %> <% recordings = local_assigns[:recordings] %> <% entitlements = local_assigns[:entitlements] %> +<% club_owner = club.owner %> +<% extra_staff = staff_memberships.reject { |ut| ut.user_id == club_owner&.id } %>
- <% if can_manage %> - <% if owner_membership&.staff_kind.blank? %> -
-

<%= t("team.streaming_staff.self_account_heading", email: current_user.email) %>

-

- <%= raw t("team.streaming_staff.self_account_hint") %> -

-
- <%= button_to t("team.streaming_staff.self_assign_submit"), public_team_assign_self_staff_path(team), method: :post, params: { staff_kind: "transmission" }, class: "btn btn-primary" %> -
-
- <% end %> + <% if club_owner %> +
+

<%= t("team.streaming_staff.principal_heading") %>

+

+ <%= raw t("team.streaming_staff.principal_body_html", email: club_owner.email) %> +

+

+ <%= raw t("team.streaming_staff.principal_score_hint_html") %> +

+
<% end %> -

<%= t("team.streaming_staff.staff_heading") %>

-
- <% if staff_memberships.any? %> + <% if extra_staff.any? %> +

<%= t("team.streaming_staff.extra_staff_heading") %>

+
<% if can_manage %><% end %> - <% staff_memberships.each do |ut| %> + <% extra_staff.each do |ut| %> <% if can_manage %> @@ -48,13 +46,8 @@ <% end %>
<%= t("team.streaming_staff.col_name") %><%= t("team.streaming_staff.col_email") %><%= t("team.streaming_staff.col_role") %>
<%= ut.user.name %> @@ -37,9 +37,7 @@ <%= t("team.streaming_staff.role_transmission") %> - <% if ut.user_id == current_user.id && team.club.owned_by?(current_user) %> - <%= button_to t("team.streaming_staff.clear_self_staff"), public_team_clear_self_staff_path(team), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem", form: { data: { turbo_confirm: t("team.streaming_staff.clear_self_staff_confirm"), confirm_kind: "delete" } } %> - <% elsif ut.role == "member" %> + <% if ut.role == "member" %> <%= button_to t("team.streaming_staff.revoke"), public_team_remove_member_path(team, ut.user), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem", form: { data: { turbo_confirm: t("team.streaming_staff.revoke_confirm", name: ut.user.name), confirm_kind: "delete" } } %> <% end %>
- <% else %> -

- <%= t("team.streaming_staff.no_staff") %> - <% if can_manage %><%= link_to t("team.streaming_staff.invite_someone"), "#invita-trasmissione" %>.<% end %> -

- <% end %> -
+
+ <% end %> <% if can_manage %>
diff --git a/backend/app/views/shared/_pending_bank_transfer.html.erb b/backend/app/views/shared/_pending_bank_transfer.html.erb index a1b7942..9aaf392 100644 --- a/backend/app/views/shared/_pending_bank_transfer.html.erb +++ b/backend/app/views/shared/_pending_bank_transfer.html.erb @@ -1,15 +1,10 @@ <%# locals: (order:) %> <% return if order.blank? %>
-

+

<%= t("billing.bank_transfer.pending_title") %> <%= raw t("billing.bank_transfer.pending_body_html", plan: order.plan.name, - price: order.price_label, - causal: order.payment_causal) %> -

-

- <%= raw t("billing.bank_transfer.pending_proof_html", - email_link: mail_to(MatchLiveTv.bank_transfer_proof_email, MatchLiveTv.bank_transfer_proof_email)) %> + price: order.price_label) %>

diff --git a/backend/app/views/shared/_plan_cards.html.erb b/backend/app/views/shared/_plan_cards.html.erb index fd84c5d..0e0a21a 100644 --- a/backend/app/views/shared/_plan_cards.html.erb +++ b/backend/app/views/shared/_plan_cards.html.erb @@ -109,7 +109,7 @@ public_club_billing_bank_transfer_path(club, plan: plan.slug, interval: interval), method: :post, class: "btn btn-outline plan-interval-btn", - form: { data: { turbo_confirm: t("billing.bank_transfer.hint") } } %> + form: { data: { turbo_confirm: t("billing.bank_transfer.confirm") } } %> <% end %>

<%= t("billing.bank_transfer.hint") %>

diff --git a/backend/app/views/shared/_quoted_price_banner.html.erb b/backend/app/views/shared/_quoted_price_banner.html.erb index 758ed5a..915cdee 100644 --- a/backend/app/views/shared/_quoted_price_banner.html.erb +++ b/backend/app/views/shared/_quoted_price_banner.html.erb @@ -1,11 +1,19 @@ -<%# locals: (quote:) %> +<%# locals: (quote:, subscription: nil) %> <% return if quote.blank? %> +<% quoted_plan_active = current_paid_plan?(subscription, quote.plan) %>

- <%= t("billing.bank_transfer.quote_banner_title") %> - <%= raw t("billing.bank_transfer.quote_banner_body_html", plan: quote.plan.name, price: quote.price_label) %> + <% if quoted_plan_active %> + <%= t("billing.bank_transfer.quote_banner_active_title") %> + <%= raw t( + "billing.bank_transfer.quote_banner_active_body_html", + plan: quote.plan.name, + price: quote.price_label, + date: subscription&.current_period_end.present? ? l_local(subscription.current_period_end.to_date) : t("billing.subscription_status.end_of_period") + ) %> + <% else %> + <%= t("billing.bank_transfer.quote_banner_title") %> + <%= raw t("billing.bank_transfer.quote_banner_body_html", plan: quote.plan.name, price: quote.price_label) %> + <% end %>

- <% if quote.note.present? %> -

<%= t("billing.bank_transfer.quote_note", note: quote.note) %>

- <% end %>
diff --git a/backend/config/initializers/match_live_tv.rb b/backend/config/initializers/match_live_tv.rb index b738a8b..8e44d60 100644 --- a/backend/config/initializers/match_live_tv.rb +++ b/backend/config/initializers/match_live_tv.rb @@ -97,6 +97,21 @@ module MatchLiveTv bank_transfer_iban.present? && bank_transfer_account_holder.present? end + def smtp_configured? + ENV["SMTP_ADDRESS"].present? + end + + # In produzione senza SMTP non blocca il flusso (es. collaudo): la mail si può inviare a mano. + def deliver_mail(mail) + if Rails.env.production? && !smtp_configured? + Rails.logger.info("[mail] skip (SMTP assente): #{mail.subject}") + return false + end + + mail.deliver_now + true + end + def privacy_controller_name ENV.fetch("PRIVACY_CONTROLLER_NAME", "Emiliano Frascaro") end diff --git a/backend/config/locales/admin.it.yml b/backend/config/locales/admin.it.yml index 04b6d79..75581b0 100644 --- a/backend/config/locales/admin.it.yml +++ b/backend/config/locales/admin.it.yml @@ -309,6 +309,8 @@ it: club: Società plan: Piano amount: Importo + holder: Intestatario + iban: IBAN causal: Causale requested: Richiesto confirm_button: Conferma pagamento e attiva diff --git a/backend/config/locales/app.de.yml b/backend/config/locales/app.de.yml index 0fb5a17..56a9578 100644 --- a/backend/config/locales/app.de.yml +++ b/backend/config/locales/app.de.yml @@ -83,11 +83,9 @@ de: first_team_name_placeholder: "z. B. Under 15" plan_label: "Starttarif (für den gesamten Verein)" plan_free: "Free — 1 Streaming-Konto pro Team, 1 Livestream" - plan_light: Premium Light - plan_full: Premium Full - interval_label: Premium-Abrechnung - interval_yearly: "Jährlich — 59 €/Jahr (Light) oder 199 €/Jahr (Full)" - interval_monthly: "Monatlich — 7,90 €/Monat (Light) oder 24,90 €/Monat (Full)" + plan_light: "Premium Light — 59 €/Jahr" + plan_full: "Premium Full — 199 €/Jahr" + plan_hint: "Karte, Überweisung oder monatliche Abrechnung wählst du danach auf der Abo-Seite. Ein kostenpflichtiger Tarif wird erst nach der Zahlung aktiv." submit: Verein erstellen billing: title: "Abonnement — %{name}" @@ -193,6 +191,10 @@ de: contact_trailer: für ein maßgeschneidertes Angebot. club_payments_link: Zahlungen und Rechnungen des Vereins streaming_staff: + principal_heading: Hauptkonto + principal_body_html: "Das Konto, mit dem der Verein registriert wurde (%{email}), ist für dieses Team vorgesehen: damit meldest du dich in der App an und startest den Livestream." + principal_score_hint_html: "Für den Punktestand von einem anderen Telefon teile während des Streams den Regie-Link (kein zweites Konto nötig)." + extra_staff_heading: Weitere Konten, die übertragen können self_account_heading: "Dein Konto (%{email})" self_account_hint: "Du kannst die Übertragung aus der App verwalten. Für den Punktestand von einem anderen Telefon teile den Regie-Link während des Streams (kein zweites Konto nötig)." self_assign_submit: Das mache ich — Übertragungsverantwortlicher @@ -613,6 +615,7 @@ de: clubs: already_registered: Du hast bereits einen Verein registriert created: Verein und erstes Team erstellt. + created_complete_subscription: Verein erstellt. Schließe das Abo auf dieser Seite ab. updated: Verein aktualisiert. complete_billing_first: Vervollständige deine Rechnungsdaten, bevor du einen Premium-Tarif aktivierst. comped_change_denied: "Dieser Tarif ist ein von Match Live TV verwaltetes kostenloses Abonnement. Um zu einem kostenpflichtigen Abonnement zu wechseln, kontaktiere den Support." diff --git a/backend/config/locales/app.en.yml b/backend/config/locales/app.en.yml index b7c6f71..38d9a00 100644 --- a/backend/config/locales/app.en.yml +++ b/backend/config/locales/app.en.yml @@ -78,11 +78,9 @@ en: first_team_name_placeholder: "e.g. Under 15" plan_label: "Starting plan (for the whole club)" plan_free: "Free — 1 transmitter account per team, 1 live stream" - plan_light: Premium Light - plan_full: Premium Full - interval_label: Premium billing - interval_yearly: "Yearly — €59/year (Light) or €199/year (Full)" - interval_monthly: "Monthly — €7.90/month (Light) or €24.90/month (Full)" + plan_light: "Premium Light — €59/year" + plan_full: "Premium Full — €199/year" + plan_hint: "Card, bank transfer or monthly billing are chosen later on the Subscription page. A paid plan activates only after payment." submit: Create club billing: title: "Subscription — %{name}" @@ -188,6 +186,10 @@ en: contact_trailer: for a custom offer. club_payments_link: Club payments and invoices streaming_staff: + principal_heading: Main account + principal_body_html: "The account used to register the club (%{email}) is the one designated for this team: use it in the app to start the live stream." + principal_score_hint_html: "For scoring from another phone, share the control-room link during the stream (no second account needed)." + extra_staff_heading: Other accounts that can broadcast self_account_heading: "Your account (%{email})" self_account_hint: "You can manage streaming from the app. For scoring from another phone, share the control-room link during the stream (no second account needed)." self_assign_submit: I'll do it — broadcast manager @@ -608,6 +610,7 @@ en: clubs: already_registered: You've already registered a club created: Club and first team created. + created_complete_subscription: Club created. Complete your subscription on this page. updated: Club updated. complete_billing_first: Complete your billing details before activating a premium plan. comped_change_denied: "This plan is a complimentary subscription managed by Match Live TV. To switch to a paid subscription, contact support." diff --git a/backend/config/locales/app.es.yml b/backend/config/locales/app.es.yml index 7958dee..5f8d7c2 100644 --- a/backend/config/locales/app.es.yml +++ b/backend/config/locales/app.es.yml @@ -83,11 +83,9 @@ es: first_team_name_placeholder: "ej. Sub-15" plan_label: "Plan inicial (para todo el club)" plan_free: "Free — 1 cuenta de emisión por equipo, 1 directo" - plan_light: Premium Light - plan_full: Premium Full - interval_label: Facturación premium - interval_yearly: "Anual — 59 €/año (Light) o 199 €/año (Full)" - interval_monthly: "Mensual — 7,90 €/mes (Light) o 24,90 €/mes (Full)" + plan_light: "Premium Light — 59 €/año" + plan_full: "Premium Full — 199 €/año" + plan_hint: "La tarjeta, la transferencia o la facturación mensual se eligen después, en Abono. El plan de pago se activa solo tras el pago." submit: Crear club billing: title: "Suscripción — %{name}" @@ -193,6 +191,10 @@ es: contact_trailer: para una oferta a medida. club_payments_link: Pagos y facturas del club streaming_staff: + principal_heading: Cuenta principal + principal_body_html: "La cuenta con la que se registró el club (%{email}) es la designada para este equipo: con ella entras en la app y arrancas el directo." + principal_score_hint_html: "Para el marcador desde otro teléfono, durante el directo comparte el enlace de control (no hace falta una segunda cuenta)." + extra_staff_heading: Otras cuentas que pueden transmitir self_account_heading: "Tu cuenta (%{email})" self_account_hint: "Puedes gestionar la transmisión desde la app. Para el marcador desde otro teléfono, comparte el enlace de control durante el directo (no hace falta una segunda cuenta)." self_assign_submit: Yo me encargo — responsable de transmisión @@ -613,6 +615,7 @@ es: clubs: already_registered: Ya has registrado un club created: Club y primer equipo creados. + created_complete_subscription: Club creado. Completa la suscripción en esta página. updated: Club actualizado. complete_billing_first: Completa tus datos de facturación antes de activar un plan premium. comped_change_denied: "Este plan es una suscripción de cortesía gestionada por Match Live TV. Para pasar a una suscripción de pago, contacta con soporte." diff --git a/backend/config/locales/app.fr.yml b/backend/config/locales/app.fr.yml index e7f35de..50759c3 100644 --- a/backend/config/locales/app.fr.yml +++ b/backend/config/locales/app.fr.yml @@ -83,11 +83,9 @@ fr: first_team_name_placeholder: "ex. Under 15" plan_label: "Forfait de départ (pour tout le club)" plan_free: "Gratuit — 1 compte diffuseur par équipe, 1 direct" - plan_light: Premium Light - plan_full: Premium Full - interval_label: Facturation premium - interval_yearly: "Annuel — 59 €/an (Light) ou 199 €/an (Full)" - interval_monthly: "Mensuel — 7,90 €/mois (Light) ou 24,90 €/mois (Full)" + plan_light: "Premium Light — 59 €/an" + plan_full: "Premium Full — 199 €/an" + plan_hint: "Carte, virement ou mensualité se choisissent ensuite, sur la page Abonnement. Le forfait payant ne s'active qu'après le paiement." submit: Créer le club billing: title: "Abonnement — %{name}" @@ -193,6 +191,10 @@ fr: contact_trailer: pour une offre sur mesure. club_payments_link: Paiements et factures du club streaming_staff: + principal_heading: Compte principal + principal_body_html: "Le compte utilisé pour enregistrer le club (%{email}) est celui désigné pour cette équipe : c’est avec lui que tu te connectes à l’app et que tu lances le direct." + principal_score_hint_html: "Pour le score depuis un autre téléphone, pendant le direct partage le lien régie (pas besoin d’un second compte)." + extra_staff_heading: Autres comptes autorisés à diffuser self_account_heading: "Ton compte (%{email})" self_account_hint: "Tu peux gérer la diffusion depuis l'application. Pour le score depuis un autre téléphone, partage le lien régie pendant le direct (pas besoin d'un second compte)." self_assign_submit: C'est moi — responsable diffusion @@ -613,6 +615,7 @@ fr: clubs: already_registered: Tu as déjà inscrit un club created: Club et première équipe créés. + created_complete_subscription: Club créé. Termine l'abonnement sur cette page. updated: Club mis à jour. complete_billing_first: Complète tes données de facturation avant d'activer un forfait premium. comped_change_denied: "Ce forfait est un abonnement offert géré par Match Live TV. Pour passer à un abonnement payant, contacte le support." diff --git a/backend/config/locales/app.it.yml b/backend/config/locales/app.it.yml index fd263f4..b995de5 100644 --- a/backend/config/locales/app.it.yml +++ b/backend/config/locales/app.it.yml @@ -78,11 +78,9 @@ it: first_team_name_placeholder: "es. Under 15" plan_label: "Piano iniziale (per tutta la società)" plan_free: "Free — 1 account trasmettitore per squadra, 1 live" - plan_light: Premium Light - plan_full: Premium Full - interval_label: Fatturazione premium - interval_yearly: "Annuale — €59/anno (Light) o €199/anno (Full)" - interval_monthly: "Mensile — €7,90/mese (Light) o €24,90/mese (Full)" + plan_light: "Premium Light — €59/anno" + plan_full: "Premium Full — €199/anno" + plan_hint: "Carta, bonifico o cadenza mensile li scegli dopo, nella pagina Abbonamento. Il piano a pagamento si attiva solo dopo il pagamento." submit: Crea società billing: title: "Abbonamento — %{name}" @@ -188,6 +186,10 @@ it: contact_trailer: per un'offerta su misura. club_payments_link: Pagamenti e fatture della società streaming_staff: + principal_heading: Account principale + principal_body_html: "L'account con cui è stata registrata la società (%{email}) è quello designato per questa squadra: con quello accedi all'app e avvii la diretta." + principal_score_hint_html: "Per il punteggio da un altro telefono, durante la diretta condividi il link regia (non serve un secondo account)." + extra_staff_heading: Altri account che possono trasmettere self_account_heading: "Il tuo account (%{email})" self_account_hint: "Puoi gestire la trasmissione dall'app. Per il punteggio da un altro telefono, condividi il link regia durante la diretta (non serve un secondo account)." self_assign_submit: Sono io — responsabile trasmissione @@ -330,7 +332,7 @@ it: sdi_or_pec: "Codice destinatario SDI (7 caratteri) o PEC" sdi_invalid: "Codice destinatario SDI (7 caratteri)" actions: - current_plan: Piano attuale + current_plan: Piano attivo contact_for_free: "Per passare al piano Free, contatta il supporto." stripe_not_configured: Stripe non configurato stripe_prices_not_configured: Prezzi Stripe non configurati @@ -436,13 +438,15 @@ it: pay_button: Paga con bonifico pay_button_with_price: "Paga con bonifico — %{price}" or_label: oppure - pending_title: Bonifico in attesa di conferma - pending_body_html: "Hai richiesto %{plan} (%{price}). Il piano si attiverà dopo la conferma del pagamento. Causale: %{causal}." - pending_proof_html: "Invia la distinta a %{email_link}." + pending_title: Ordine in corso + pending_body_html: "Hai richiesto %{plan} (%{price}). Resta in attesa: il piano si attiverà quando Match Live TV confermerà il pagamento." quote_banner_title: Prezzo concordato quote_banner_body_html: "Per questa società vale %{plan} a %{price}. Il pagamento avviene solo con bonifico (non con carta)." + quote_banner_active_title: Piano attivo + quote_banner_active_body_html: "In questo momento vale %{plan} a %{price}, fino al %{date}. Il rinnovo non è automatico." quote_note: "Nota commerciale: %{note}" hint: "Il piano resta quello attuale finché Match Live TV non conferma l'accredito." + confirm: "Confermi la richiesta di bonifico? Il piano resta quello attuale finché Match Live TV non conferma l'accredito." renewal_html: "Con il bonifico l'abbonamento non si rinnova da solo: alla scadenza torna Free, salvo nuovo bonifico o rinnovo da parte nostra." regia: meta_title_fallback: "Regia — Match Live TV" @@ -627,6 +631,7 @@ it: clubs: already_registered: Hai già registrato una società created: Società e prima squadra create. + created_complete_subscription: Società creata. Completa l'abbonamento in questa pagina. updated: Società aggiornata. complete_billing_first: Completa i dati di fatturazione prima di attivare un piano premium. comped_change_denied: "Il piano è un abbonamento omaggio gestito da Match Live TV. Per passare a un abbonamento a pagamento contatta il supporto." @@ -646,7 +651,7 @@ it: subscription_canceled: "Abbonamento disdetto. Resta attivo fino al %{date}; da quel giorno passerai al piano Free." stripe_error: "Errore Stripe: %{message}" invoice_pdf_unavailable: PDF fattura non disponibile. - bank_transfer_requested: Istruzioni per il bonifico inviate all'email di fatturazione. Il piano si attiverà dopo la conferma del pagamento. + bank_transfer_requested: Richiesta registrata. Il piano si attiverà dopo la conferma del pagamento. quote_checkout_denied: "Per questa società è attivo un prezzo concordato: usa il pagamento con bonifico." club_recordings: not_authorized: Non autorizzato diff --git a/backend/public/confirm-forms.js b/backend/public/confirm-forms.js index a944176..14926b8 100644 --- a/backend/public/confirm-forms.js +++ b/backend/public/confirm-forms.js @@ -147,12 +147,13 @@ var hasSite = options.hasSite !== false; var hasYoutube = !!options.hasYoutube; var isDelete = !!options.isDelete; + var isReplayDelete = !!options.isReplayDelete; var canSite = hasSite; var canYoutube = hasYoutube; var canBoth = hasSite && hasYoutube; var choiceCount = (canSite ? 1 : 0) + (canYoutube ? 1 : 0) + (canBoth ? 1 : 0); // Una sola destinazione disponibile → conferma semplice, senza elenco - var multi = !!options.multi && choiceCount > 1; + var multi = isReplayDelete && choiceCount > 1; if (canBoth) pendingScope = "both"; else if (canSite) pendingScope = "site"; @@ -175,9 +176,9 @@ if (titleEl) { if (multi) { titleEl.textContent = I18N.multiDeleteTitle; - } else if (pendingScope === "youtube") { + } else if (isReplayDelete && pendingScope === "youtube") { titleEl.textContent = I18N.deleteYoutubeTitle; - } else if (pendingScope === "site") { + } else if (isReplayDelete && pendingScope === "site") { titleEl.textContent = I18N.deleteSiteTitle; } else { titleEl.textContent = isDelete ? I18N.deleteTitle : I18N.title; @@ -187,9 +188,9 @@ if (messageEl) { if (multi) { messageEl.textContent = I18N.multiDeleteMessage; - } else if (pendingScope === "youtube") { + } else if (isReplayDelete && pendingScope === "youtube") { messageEl.textContent = I18N.deleteYoutubeMessage; - } else if (pendingScope === "site") { + } else if (isReplayDelete && pendingScope === "site") { messageEl.textContent = hasYoutube ? I18N.deleteSiteMessageWithYoutube : I18N.deleteSiteMessageOnly; @@ -203,8 +204,8 @@ if (okBtn instanceof HTMLElement) { okBtn.hidden = multi; - if (pendingScope === "youtube") okBtn.textContent = I18N.deleteYoutubeOk; - else if (pendingScope === "site") okBtn.textContent = I18N.deleteSiteOk; + if (isReplayDelete && pendingScope === "youtube") okBtn.textContent = I18N.deleteYoutubeOk; + else if (isReplayDelete && pendingScope === "site") okBtn.textContent = I18N.deleteSiteOk; else okBtn.textContent = isDelete ? I18N.deleteOk : I18N.confirmOk; } @@ -302,12 +303,14 @@ form.getAttribute("data-has-youtube") === "1" || mode === "delete-replay-youtube"; var hasSiteAttr = form.getAttribute("data-has-site"); - var hasSite = hasSiteAttr == null ? true : hasSiteAttr === "1"; + var hasSite = isReplayDelete + ? hasSiteAttr == null || hasSiteAttr === "1" + : hasSiteAttr === "1"; var isDelete = form.getAttribute("data-confirm-kind") === "delete" || isReplayDelete; openDialog(message, { - multi: isReplayDelete, + isReplayDelete: isReplayDelete, hasYoutube: hasYoutube, hasSite: hasSite, isDelete: isDelete diff --git a/backend/spec/helpers/public/billing_helper_spec.rb b/backend/spec/helpers/public/billing_helper_spec.rb index 6ad9392..b046da2 100644 --- a/backend/spec/helpers/public/billing_helper_spec.rb +++ b/backend/spec/helpers/public/billing_helper_spec.rb @@ -76,4 +76,72 @@ RSpec.describe Public::BillingHelper, type: :helper do ) expect(action[:kind]).to eq(:none) end + + it "con prezzo concordato non pagato resta sul bonifico" do + quote = instance_double(Billing::ClubQuote, plan_slug: "premium_full", price_label: "€99/anno", billing_interval: "yearly") + club = instance_double(Club, active_billing_quote: quote) + subscription = instance_double( + Subscription, + active?: true, + premium?: false, + plan_change_pending?: false, + plan: Plan["free"] + ) + + action = helper.plan_billing_action( + current_slug: "free", + target_plan: premium_full, + stripe_subscription_active: false, + subscription: subscription, + club: club + ) + expect(action[:kind]).to eq(:quoted) + expect(action[:quote]).to eq(quote) + end + + it "con prezzo concordato già pagato mostra il piano attivo" do + quote = instance_double(Billing::ClubQuote, plan_slug: "premium_full", price_label: "€99/anno") + club = instance_double(Club, active_billing_quote: quote) + subscription = instance_double( + Subscription, + active?: true, + premium?: true, + plan_change_pending?: false, + plan: premium_full, + billing_interval: "yearly" + ) + + action = helper.plan_billing_action( + current_slug: "premium_full", + target_plan: premium_full, + stripe_subscription_active: false, + current_interval: "yearly", + subscription: subscription, + club: club + ) + expect(action[:kind]).to eq(:current) + expect(action[:label]).to include("Piano attivo") + expect(action[:label]).to include("€99/anno") + end + + it "con bonifico già attivo senza Stripe mostra il piano corrente" do + subscription = instance_double( + Subscription, + active?: true, + premium?: true, + plan_change_pending?: false, + plan: premium_full, + billing_interval: "yearly" + ) + + action = helper.plan_billing_action( + current_slug: "premium_full", + target_plan: premium_full, + stripe_subscription_active: false, + current_interval: "yearly", + subscription: subscription + ) + expect(action[:kind]).to eq(:current) + expect(action[:label]).to include("Piano attivo") + end end diff --git a/backend/spec/requests/public/club_bank_transfer_spec.rb b/backend/spec/requests/public/club_bank_transfer_spec.rb index 1e996fe..54fc13e 100644 --- a/backend/spec/requests/public/club_bank_transfer_spec.rb +++ b/backend/spec/requests/public/club_bank_transfer_spec.rb @@ -57,6 +57,33 @@ RSpec.describe "Public bank transfer billing", type: :request do get public_club_billing_path(club) expect(response.body).to include("Prezzo concordato") expect(response.body).to include("€120") + expect(response.body).to include("Paga con bonifico") expect(response.body).not_to include("Attiva Full — €199/anno") end + + it "con prezzo concordato già attivato mostra Piano attivo e nasconde il pagamento" do + Billing::SetClubQuote.upsert( + club: club, plan_slug: "premium_full", interval: "yearly", + amount_euros: "99", note: "Test", admin: nil + ) + Billing::AssignPlan.call( + club: club, + plan_slug: "premium_full", + status: "active", + stripe_attrs: { + stripe_subscription_id: nil, + billing_interval: "yearly", + current_period_start: Time.current, + current_period_end: 1.year.from_now, + cancel_at_period_end: true + } + ) + + get public_club_billing_path(club) + expect(response.body).to include("Piano attivo") + expect(response.body).to include("In questo momento vale") + expect(response.body).to include("€99") + expect(response.body).not_to include("Paga con bonifico") + expect(response.body).not_to include("Il piano resta quello attuale finché Match Live TV non conferma") + end end diff --git a/backend/spec/requests/public/club_create_spec.rb b/backend/spec/requests/public/club_create_spec.rb new file mode 100644 index 0000000..816bf39 --- /dev/null +++ b/backend/spec/requests/public/club_create_spec.rb @@ -0,0 +1,56 @@ +require "rails_helper" + +RSpec.describe "Public club create", type: :request do + let!(:coach) do + User.create!(email: "create-coach@test.it", name: "Coach", password: "Password123", role: "coach") + end + + def login! + post public_login_path, params: { email: coach.email, password: "Password123" } + end + + def create_params(plan:) + { + club: { + name: "Nuova Società", + sport: "pallavolo", + primary_color: "#e53935", + secondary_color: "#ffffff" + }, + first_team: { name: "Under 15" }, + plan: plan + } + end + + it "mostra solo il piano iniziale, senza cadenza" do + login! + get public_new_club_path + expect(response).to have_http_status(:ok) + expect(response.body).to include("Piano iniziale") + expect(response.body).not_to include("Fatturazione premium") + end + + it "crea la società sul piano Free" do + login! + expect { + post public_clubs_path, params: create_params(plan: "free") + }.to change(Club, :count).by(1) + + club = Club.order(:created_at).last + expect(response).to redirect_to(public_club_path(club)) + expect(club.subscription.plan.slug).to eq("free") + team = club.teams.last + membership = coach.user_teams.find_by(team: team) + expect(membership&.staff_kind).to eq("transmission") + end + + it "se sceglie Full senza Stripe resta Free e va in billing" do + login! + allow(MatchLiveTv).to receive(:stripe_enabled?).and_return(false) + + post public_clubs_path, params: create_params(plan: "premium_full") + club = Club.order(:created_at).last + expect(club.subscription.plan.slug).to eq("free") + expect(response).to redirect_to(public_club_billing_path(club)) + end +end diff --git a/backend/spec/requests/public/team_invite_spec.rb b/backend/spec/requests/public/team_invite_spec.rb index 7d8c98b..35d69e4 100644 --- a/backend/spec/requests/public/team_invite_spec.rb +++ b/backend/spec/requests/public/team_invite_spec.rb @@ -22,9 +22,13 @@ RSpec.describe "Public team transmission invite", type: :request do get public_team_details_path(team) expect(response).to have_http_status(:ok) + expect(response.body).to include("Account principale") + expect(response.body).to include("designato per questa squadra") + expect(response.body).to include(coach.email) expect(response.body).to include("Invita un account a trasmettere") expect(response.body).to include('id="invita-trasmissione"') - expect(response.body).to include("#responsabili-trasmissione") + expect(response.body).not_to include("Sono io — responsabile trasmissione") + expect(response.body).not_to include("Nessun responsabile trasmissione") end it "GET /teams/:id/invite reindirizza ai dettagli con ancora" do diff --git a/backend/spec/services/billing/request_bank_transfer_spec.rb b/backend/spec/services/billing/request_bank_transfer_spec.rb index 7068db0..0d500ca 100644 --- a/backend/spec/services/billing/request_bank_transfer_spec.rb +++ b/backend/spec/services/billing/request_bank_transfer_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Billing::RequestBankTransfer do ) end - it "crea un ordine a listino e invia le istruzioni" do + it "crea l'ordine a listino e invia le istruzioni" do expect { described_class.call(club: club, user: user, plan_slug: "premium_light", interval: "yearly") }.to change { club.billing_transfer_orders.count }.by(1) @@ -41,6 +41,18 @@ RSpec.describe Billing::RequestBankTransfer do expect(club.reload.subscription.plan.slug).to eq("free") end + it "crea comunque l'ordine se in produzione manca SMTP" do + allow(MatchLiveTv).to receive(:smtp_configured?).and_return(false) + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production")) + + expect { + described_class.call(club: club, user: user, plan_slug: "premium_light", interval: "yearly") + }.to change { club.billing_transfer_orders.count }.by(1) + .and change { ActionMailer::Base.deliveries.size }.by(0) + + expect(club.billing_transfer_orders.last).to be_awaiting_payment + end + it "usa l'importo concordato se presente" do Billing::SetClubQuote.upsert( club: club, plan_slug: "premium_full", interval: "yearly", diff --git a/backend/spec/services/teams/staff_assignment_spec.rb b/backend/spec/services/teams/staff_assignment_spec.rb index b408208..192347e 100644 --- a/backend/spec/services/teams/staff_assignment_spec.rb +++ b/backend/spec/services/teams/staff_assignment_spec.rb @@ -19,6 +19,12 @@ RSpec.describe Teams::StaffAssignment do expect(team.entitlements.staff_count_for("transmission")).to eq(1) end + it "designa il titolare della società sulla squadra" do + described_class.designate_club_owner!(team: team, user: owner) + membership = owner.user_teams.find_by!(team: team) + expect(membership.staff_kind).to eq("transmission") + end + it "rejects duplicate email for transmission" do UserTeam.create!(user: other, team: team, role: "member", staff_kind: "transmission") expect {