Aggiunge abbonamenti omaggio admin per sponsor e promozioni.
Gli admin possono concedere o revocare Premium Light/Full senza Stripe; il piano omaggio ha priorità sui webhook e la società vede un messaggio dedicato. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
45
backend/app/controllers/admin/clubs_controller.rb
Normal file
45
backend/app/controllers/admin/clubs_controller.rb
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
module Admin
|
||||||
|
class ClubsController < BaseController
|
||||||
|
before_action :set_club, only: %i[show grant_comped revoke_comped]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@clubs = Club.includes(:subscription, subscription: :plan, subscription: :admin_comped_by)
|
||||||
|
.order(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@subscription = @club.subscription || @club.build_subscription(plan: Plan["free"], status: "active")
|
||||||
|
@plans = Plan.ordered.reject { |p| p.slug == "free" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def grant_comped
|
||||||
|
Billing::AdminCompedSubscription.grant(
|
||||||
|
club: @club,
|
||||||
|
plan_slug: params.require(:plan_slug),
|
||||||
|
reason: params[:reason],
|
||||||
|
admin: current_admin_account
|
||||||
|
)
|
||||||
|
redirect_back_or_club notice: "Abbonamento omaggio #{Plan[params[:plan_slug]].name} attivato per #{@club.name}."
|
||||||
|
rescue Billing::AdminCompedSubscription::Error, ActiveRecord::RecordInvalid => e
|
||||||
|
redirect_back_or_club alert: e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke_comped
|
||||||
|
Billing::AdminCompedSubscription.revoke(club: @club, admin: current_admin_account)
|
||||||
|
redirect_back_or_club notice: "Abbonamento omaggio revocato per #{@club.name}."
|
||||||
|
rescue Billing::AdminCompedSubscription::Error => e
|
||||||
|
redirect_back_or_club alert: e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_club
|
||||||
|
@club = Club.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_back_or_club(notice: nil, alert: nil)
|
||||||
|
target = params[:return_to].presence || admin_club_path(@club)
|
||||||
|
redirect_to target, notice: notice, alert: alert
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -85,6 +85,12 @@ module Public
|
|||||||
|
|
||||||
def checkout
|
def checkout
|
||||||
require_club_owner!(@club)
|
require_club_owner!(@club)
|
||||||
|
if @club.subscription&.admin_comped?
|
||||||
|
redirect_to public_club_billing_path(@club),
|
||||||
|
alert: "Il piano è un abbonamento omaggio gestito da Match Live TV. Per passare a un abbonamento a pagamento contatta il supporto."
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
unless MatchLiveTv.stripe_enabled?
|
unless MatchLiveTv.stripe_enabled?
|
||||||
redirect_to public_club_billing_path(@club), alert: "Pagamenti non ancora configurati sul server"
|
redirect_to public_club_billing_path(@club), alert: "Pagamenti non ancora configurati sul server"
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class Subscription < ApplicationRecord
|
|||||||
belongs_to :club
|
belongs_to :club
|
||||||
belongs_to :plan
|
belongs_to :plan
|
||||||
belongs_to :pending_plan, class_name: "Plan", optional: true
|
belongs_to :pending_plan, class_name: "Plan", optional: true
|
||||||
|
belongs_to :admin_comped_by, class_name: "AdminAccount", optional: true
|
||||||
|
|
||||||
validates :status, inclusion: { in: STATUSES }
|
validates :status, inclusion: { in: STATUSES }
|
||||||
validates :club_id, uniqueness: true
|
validates :club_id, uniqueness: true
|
||||||
@@ -30,4 +31,8 @@ class Subscription < ApplicationRecord
|
|||||||
def plan_change_pending?
|
def plan_change_pending?
|
||||||
pending_plan_id.present?
|
pending_plan_id.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def admin_comped?
|
||||||
|
admin_comped
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
101
backend/app/services/billing/admin_comped_subscription.rb
Normal file
101
backend/app/services/billing/admin_comped_subscription.rb
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
module Billing
|
||||||
|
class AdminCompedSubscription
|
||||||
|
class Error < StandardError; end
|
||||||
|
|
||||||
|
VALID_PLANS = %w[premium_light premium_full].freeze
|
||||||
|
|
||||||
|
def self.grant(club:, plan_slug:, reason:, admin:)
|
||||||
|
new(club: club, plan_slug: plan_slug, reason: reason, admin: admin).grant
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.revoke(club:, admin:)
|
||||||
|
new(club: club, admin: admin).revoke
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(club:, plan_slug: nil, reason: nil, admin: nil)
|
||||||
|
@club = club
|
||||||
|
@plan_slug = plan_slug.to_s.presence
|
||||||
|
@reason = reason.to_s.strip.presence
|
||||||
|
@admin = admin
|
||||||
|
end
|
||||||
|
|
||||||
|
def grant
|
||||||
|
raise Error, "Piano non valido" unless @plan_slug.in?(VALID_PLANS)
|
||||||
|
|
||||||
|
release_stripe_schedule!
|
||||||
|
|
||||||
|
AssignPlan.call(
|
||||||
|
club: @club,
|
||||||
|
plan_slug: @plan_slug,
|
||||||
|
status: "active",
|
||||||
|
stripe_attrs: comped_attrs.merge(
|
||||||
|
pending_plan_id: nil,
|
||||||
|
pending_billing_interval: nil,
|
||||||
|
stripe_schedule_id: nil,
|
||||||
|
cancel_at_period_end: false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke
|
||||||
|
sub = @club.subscription
|
||||||
|
raise Error, "Nessun abbonamento omaggio attivo" unless sub&.admin_comped?
|
||||||
|
|
||||||
|
if sub.stripe_subscription_id.present? && MatchLiveTv.stripe_enabled?
|
||||||
|
stripe_sub = ::Stripe::Subscription.retrieve(sub.stripe_subscription_id)
|
||||||
|
if stripe_sub.status.in?(%w[active trialing past_due])
|
||||||
|
Billing::Stripe::CheckoutSync.from_subscription(stripe_sub, club: @club)
|
||||||
|
clear_comped_flags!(sub.reload)
|
||||||
|
return sub
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
AssignPlan.call(
|
||||||
|
club: @club,
|
||||||
|
plan_slug: "free",
|
||||||
|
status: "active",
|
||||||
|
stripe_attrs: {
|
||||||
|
admin_comped: false,
|
||||||
|
admin_comped_reason: nil,
|
||||||
|
admin_comped_at: nil,
|
||||||
|
admin_comped_by_id: nil,
|
||||||
|
pending_plan_id: nil,
|
||||||
|
pending_billing_interval: nil,
|
||||||
|
stripe_schedule_id: nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def comped_attrs
|
||||||
|
{
|
||||||
|
admin_comped: true,
|
||||||
|
admin_comped_reason: @reason,
|
||||||
|
admin_comped_at: Time.current,
|
||||||
|
admin_comped_by_id: @admin&.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_comped_flags!(sub)
|
||||||
|
sub.update!(
|
||||||
|
admin_comped: false,
|
||||||
|
admin_comped_reason: nil,
|
||||||
|
admin_comped_at: nil,
|
||||||
|
admin_comped_by_id: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_stripe_schedule!
|
||||||
|
sub = @club.subscription
|
||||||
|
return if sub.blank? || sub.stripe_schedule_id.blank?
|
||||||
|
return unless MatchLiveTv.stripe_enabled?
|
||||||
|
|
||||||
|
::Stripe::SubscriptionSchedule.release(sub.stripe_schedule_id)
|
||||||
|
rescue ::Stripe::InvalidRequestError
|
||||||
|
nil
|
||||||
|
ensure
|
||||||
|
sub&.update!(stripe_schedule_id: nil, pending_plan_id: nil, pending_billing_interval: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -18,6 +18,7 @@ module Billing
|
|||||||
raise "Stripe non configurato" unless MatchLiveTv.stripe_enabled?
|
raise "Stripe non configurato" unless MatchLiveTv.stripe_enabled?
|
||||||
raise "Piano non valido" unless VALID_PLANS.include?(@plan_slug)
|
raise "Piano non valido" unless VALID_PLANS.include?(@plan_slug)
|
||||||
|
|
||||||
|
raise "Abbonamento omaggio: contatta il supporto per cambiare piano a pagamento" if @sub&.admin_comped?
|
||||||
raise "Nessun abbonamento Stripe attivo" if @sub.blank? || @sub.stripe_subscription_id.blank?
|
raise "Nessun abbonamento Stripe attivo" if @sub.blank? || @sub.stripe_subscription_id.blank?
|
||||||
|
|
||||||
interval = resolve_interval(@sub)
|
interval = resolve_interval(@sub)
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ module Billing
|
|||||||
PriceCatalog::DEFAULT_INTERVAL
|
PriceCatalog::DEFAULT_INTERVAL
|
||||||
|
|
||||||
sub = club.subscription
|
sub = club.subscription
|
||||||
|
return sub if sub&.admin_comped?
|
||||||
|
|
||||||
clear_pending = sub&.pending_plan_id.present? &&
|
clear_pending = sub&.pending_plan_id.present? &&
|
||||||
(sub.pending_plan.slug == plan.slug || infer_plan_slug_from_price(stripe_sub) == sub.pending_plan.slug)
|
(sub.pending_plan.slug == plan.slug || infer_plan_slug_from_price(stripe_sub) == sub.pending_plan.slug)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module Billing
|
|||||||
sub = @club.subscription
|
sub = @club.subscription
|
||||||
return false unless sub&.stripe_subscription_id.present?
|
return false unless sub&.stripe_subscription_id.present?
|
||||||
return false if sub.free?
|
return false if sub.free?
|
||||||
|
return false if sub.admin_comped?
|
||||||
|
|
||||||
stripe_sub = ::Stripe::Subscription.retrieve(sub.stripe_subscription_id)
|
stripe_sub = ::Stripe::Subscription.retrieve(sub.stripe_subscription_id)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if @filter_club %>
|
||||||
|
<%= render "admin/clubs/comped_form",
|
||||||
|
club: @filter_club,
|
||||||
|
subscription: @filter_club.subscription,
|
||||||
|
return_to: admin_billing_path(club_id: @filter_club.id) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% if @pending_payments.any? %>
|
<% if @pending_payments.any? %>
|
||||||
<div class="billing-pending-list">
|
<div class="billing-pending-list">
|
||||||
<% @pending_payments.each do |payment| %>
|
<% @pending_payments.each do |payment| %>
|
||||||
|
|||||||
56
backend/app/views/admin/clubs/_comped_form.html.erb
Normal file
56
backend/app/views/admin/clubs/_comped_form.html.erb
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<%# locals: (club:, subscription:, return_to: nil) %>
|
||||||
|
<section class="admin-comped-card" style="margin-bottom:24px;padding:16px;border:1px solid #3d3520;border-radius:8px;background:#2a2618">
|
||||||
|
<h2 style="margin:0 0 8px;font-size:1.1rem">Abbonamento omaggio</h2>
|
||||||
|
<p style="color:#bbb;font-size:0.9rem;margin:0 0 14px">
|
||||||
|
Sponsor o promozione: assegna Premium Light/Full senza pagamento Stripe. Revocabile in qualsiasi momento.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% sub = subscription %>
|
||||||
|
<% if sub&.admin_comped? %>
|
||||||
|
<p style="margin:0 0 12px">
|
||||||
|
<strong>Attivo:</strong> <%= sub.plan.name %>
|
||||||
|
<% if sub.admin_comped_reason.present? %>
|
||||||
|
· <%= sub.admin_comped_reason %>
|
||||||
|
<% end %>
|
||||||
|
<% if sub.admin_comped_at.present? %>
|
||||||
|
<br><span style="color:#888;font-size:0.85rem">Dal <%= l(sub.admin_comped_at, format: :long) %>
|
||||||
|
<% if sub.admin_comped_by.present? %> · admin <%= sub.admin_comped_by.username %><% end %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% if sub.stripe_subscription_id.present? %>
|
||||||
|
<br><span style="color:#888;font-size:0.85rem">Nota: esiste anche un abbonamento Stripe collegato; l’omaggio ha priorità sul piano.</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<%= button_to "Revoca omaggio (torna Free o ripristina Stripe)",
|
||||||
|
revoke_comped_admin_club_path(club, return_to: return_to),
|
||||||
|
method: :delete,
|
||||||
|
class: "admin-btn admin-btn--secondary",
|
||||||
|
form: { data: { turbo_confirm: "Revocare l’abbonamento omaggio per #{club.name}?" } } %>
|
||||||
|
<% else %>
|
||||||
|
<p style="margin:0 0 12px;color:#888">
|
||||||
|
Piano attuale: <strong><%= sub&.plan&.name || "Free" %></strong>
|
||||||
|
<% if sub&.stripe_subscription_id.present? %>
|
||||||
|
· pagamento Stripe attivo
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<%= form_with url: grant_comped_admin_club_path(club), method: :post, local: true, class: "admin-comped-form" do %>
|
||||||
|
<%= hidden_field_tag :return_to, return_to if return_to.present? %>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end">
|
||||||
|
<label style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem">
|
||||||
|
Piano
|
||||||
|
<%= select_tag :plan_slug,
|
||||||
|
options_for_select(
|
||||||
|
[["Premium Light", "premium_light"], ["Premium Full", "premium_full"]]
|
||||||
|
),
|
||||||
|
required: true,
|
||||||
|
class: "admin-input" %>
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem;flex:1;min-width:200px">
|
||||||
|
Motivo (es. sponsor 2026)
|
||||||
|
<%= text_field_tag :reason, nil, placeholder: "Sponsor, promozione…", class: "admin-input", style: "width:100%" %>
|
||||||
|
</label>
|
||||||
|
<%= submit_tag "Concedi omaggio", class: "admin-btn admin-btn--primary" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
38
backend/app/views/admin/clubs/index.html.erb
Normal file
38
backend/app/views/admin/clubs/index.html.erb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<h2>Società</h2>
|
||||||
|
<p style="color:#888;margin-bottom:16px">
|
||||||
|
Gestisci abbonamenti omaggio (sponsor / promozioni). Per pagamenti Stripe usa <%= link_to "Fatturazione", admin_billing_path %>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Società</th>
|
||||||
|
<th>Piano</th>
|
||||||
|
<th>Omaggio</th>
|
||||||
|
<th>Stripe</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @clubs.each do |club| %>
|
||||||
|
<% sub = club.subscription %>
|
||||||
|
<tr>
|
||||||
|
<td><strong><%= club.name %></strong></td>
|
||||||
|
<td><%= sub&.plan&.name || "Free" %></td>
|
||||||
|
<td>
|
||||||
|
<% if sub&.admin_comped? %>
|
||||||
|
<span style="color:#ffb74d">Sì</span>
|
||||||
|
<% if sub.admin_comped_reason.present? %> · <%= sub.admin_comped_reason %><% end %>
|
||||||
|
<% else %>
|
||||||
|
—
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td><%= sub&.stripe_subscription_id.present? ? "Sì" : "—" %></td>
|
||||||
|
<td>
|
||||||
|
<%= link_to "Gestisci", admin_club_path(club) %>
|
||||||
|
· <%= link_to "Fatture", admin_billing_path(club_id: club.id) %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
16
backend/app/views/admin/clubs/show.html.erb
Normal file
16
backend/app/views/admin/clubs/show.html.erb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<p style="margin-bottom:16px"><%= link_to "← Tutte le società", admin_clubs_path %></p>
|
||||||
|
|
||||||
|
<h2><%= @club.name %></h2>
|
||||||
|
<p style="color:#888">
|
||||||
|
Sport: <%= @club.sport %>
|
||||||
|
· <%= link_to "Pagamenti e fatture", admin_billing_path(club_id: @club.id) %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<%= render "admin/clubs/comped_form", club: @club, subscription: @subscription, return_to: admin_club_path(@club) %>
|
||||||
|
|
||||||
|
<h3 style="font-size:1rem;margin-top:24px">Squadre</h3>
|
||||||
|
<ul>
|
||||||
|
<% @club.teams.order(:name).each do |team| %>
|
||||||
|
<li><%= link_to team.name, admin_team_path(team) %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<% if admin_logged_in? %>
|
<% if admin_logged_in? %>
|
||||||
<%= link_to "Dashboard", admin_root_path, class: ("active" if controller_name == "dashboard") %>
|
<%= link_to "Dashboard", admin_root_path, class: ("active" if controller_name == "dashboard") %>
|
||||||
<%= link_to "Teams", admin_teams_path, class: ("active" if controller_name == "teams") %>
|
<%= link_to "Teams", admin_teams_path, class: ("active" if controller_name == "teams") %>
|
||||||
|
<%= link_to "Società", admin_clubs_path, class: ("active" if controller_name == "clubs") %>
|
||||||
<%= link_to "Fatturazione", admin_billing_path, class: ("active" if controller_name.in?(%w[billing billing_invoices])) %>
|
<%= link_to "Fatturazione", admin_billing_path, class: ("active" if controller_name.in?(%w[billing billing_invoices])) %>
|
||||||
<%= link_to "YouTube", admin_youtube_platform_path, class: ("active" if controller_name == "youtube") %>
|
<%= link_to "YouTube", admin_youtube_platform_path, class: ("active" if controller_name == "youtube") %>
|
||||||
<%= link_to "Sessions", admin_sessions_path, class: ("active" if controller_name == "sessions") %>
|
<%= link_to "Sessions", admin_sessions_path, class: ("active" if controller_name == "sessions") %>
|
||||||
|
|||||||
@@ -5,12 +5,26 @@
|
|||||||
<h1>Abbonamento</h1>
|
<h1>Abbonamento</h1>
|
||||||
<%= render "shared/club_subscription_status", club: @club, entitlements: @entitlements, subscription: @subscription, on_billing_page: true %>
|
<%= render "shared/club_subscription_status", club: @club, entitlements: @entitlements, subscription: @subscription, on_billing_page: true %>
|
||||||
|
|
||||||
|
<% if @subscription&.admin_comped? %>
|
||||||
|
<div class="card" style="margin-top:16px;border-color:#3d3520;background:#1a1810">
|
||||||
|
<p style="margin:0;color:#ddd">
|
||||||
|
<strong>Abbonamento omaggio Match Live TV</strong>
|
||||||
|
— piano <strong><%= @subscription.plan.name %></strong>
|
||||||
|
<% if @subscription.admin_comped_reason.present? %>
|
||||||
|
(<%= @subscription.admin_comped_reason %>)
|
||||||
|
<% end %>.
|
||||||
|
Non è richiesto alcun pagamento. Per modifiche o passaggio a un piano a pagamento, contatta
|
||||||
|
<%= mail_to "info@matchlive.it", "info@matchlive.it" %>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
<%= render "shared/stripe_secure_payment" %>
|
<%= render "shared/stripe_secure_payment" %>
|
||||||
<% if MatchLiveTv.stripe_enabled? %>
|
<% if MatchLiveTv.stripe_enabled? %>
|
||||||
<%= render "shared/plan_change_info", subscription: @subscription %>
|
<%= render "shared/plan_change_info", subscription: @subscription %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render "shared/plan_cards", show_stripe_portal: false %>
|
<%= render "shared/plan_cards", show_stripe_portal: false %>
|
||||||
<%= render "shared/subscription_cancel", club: @club, subscription: @subscription %>
|
<%= render "shared/subscription_cancel", club: @club, subscription: @subscription %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= render "shared/billing_documents", club: @club, payments: @payments %>
|
<%= render "shared/billing_documents", club: @club, payments: @payments %>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
<p style="color:#aaa;margin-bottom:8px">
|
<p style="color:#aaa;margin-bottom:8px">
|
||||||
Società: <strong><%= club.name %></strong>
|
Società: <strong><%= club.name %></strong>
|
||||||
· Piano attuale: <strong><%= current_plan.name %></strong>
|
· Piano attuale: <strong><%= current_plan.name %></strong>
|
||||||
|
<% if subscription&.admin_comped? %>
|
||||||
|
<span style="color:#ffb74d">(omaggio Match Live TV)</span>
|
||||||
|
<% end %>
|
||||||
(valido per tutte le squadre)
|
(valido per tutte le squadre)
|
||||||
<% unless local_assigns[:on_billing_page] %>
|
<% unless local_assigns[:on_billing_page] %>
|
||||||
· <%= link_to "Gestisci abbonamento", public_club_billing_path(club) %>
|
· <%= link_to "Gestisci abbonamento", public_club_billing_path(club) %>
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ Rails.application.routes.draw do
|
|||||||
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
|
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: %i[index show] do
|
||||||
|
member do
|
||||||
|
post :grant_comped
|
||||||
|
delete :revoke_comped
|
||||||
|
end
|
||||||
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"
|
||||||
end
|
end
|
||||||
resources :sessions, only: %i[index show] do
|
resources :sessions, only: %i[index show] do
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class AddAdminCompedToSubscriptions < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
change_table :subscriptions, bulk: true do |t|
|
||||||
|
t.boolean :admin_comped, null: false, default: false
|
||||||
|
t.string :admin_comped_reason
|
||||||
|
t.datetime :admin_comped_at
|
||||||
|
t.references :admin_comped_by, type: :uuid, foreign_key: { to_table: :admin_accounts }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
8
backend/db/schema.rb
generated
8
backend/db/schema.rb
generated
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.2].define(version: 2026_06_02_120000) do
|
ActiveRecord::Schema[7.2].define(version: 2026_06_03_120000) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -253,6 +253,11 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_02_120000) do
|
|||||||
t.uuid "pending_plan_id"
|
t.uuid "pending_plan_id"
|
||||||
t.string "pending_billing_interval"
|
t.string "pending_billing_interval"
|
||||||
t.string "stripe_schedule_id"
|
t.string "stripe_schedule_id"
|
||||||
|
t.boolean "admin_comped", default: false, null: false
|
||||||
|
t.string "admin_comped_reason"
|
||||||
|
t.datetime "admin_comped_at"
|
||||||
|
t.uuid "admin_comped_by_id"
|
||||||
|
t.index ["admin_comped_by_id"], name: "index_subscriptions_on_admin_comped_by_id"
|
||||||
t.index ["club_id"], name: "index_subscriptions_on_club_id", unique: true
|
t.index ["club_id"], name: "index_subscriptions_on_club_id", unique: true
|
||||||
t.index ["pending_plan_id"], name: "index_subscriptions_on_pending_plan_id"
|
t.index ["pending_plan_id"], name: "index_subscriptions_on_pending_plan_id"
|
||||||
t.index ["plan_id"], name: "index_subscriptions_on_plan_id"
|
t.index ["plan_id"], name: "index_subscriptions_on_plan_id"
|
||||||
@@ -355,6 +360,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_02_120000) do
|
|||||||
add_foreign_key "stream_events", "stream_sessions"
|
add_foreign_key "stream_events", "stream_sessions"
|
||||||
add_foreign_key "stream_sessions", "matches"
|
add_foreign_key "stream_sessions", "matches"
|
||||||
add_foreign_key "stream_sessions", "users"
|
add_foreign_key "stream_sessions", "users"
|
||||||
|
add_foreign_key "subscriptions", "admin_accounts", column: "admin_comped_by_id"
|
||||||
add_foreign_key "subscriptions", "clubs"
|
add_foreign_key "subscriptions", "clubs"
|
||||||
add_foreign_key "subscriptions", "plans"
|
add_foreign_key "subscriptions", "plans"
|
||||||
add_foreign_key "subscriptions", "plans", column: "pending_plan_id"
|
add_foreign_key "subscriptions", "plans", column: "pending_plan_id"
|
||||||
|
|||||||
@@ -233,6 +233,24 @@ body.admin-body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-input {
|
||||||
|
padding: 0.45rem 0.6rem;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #1a1a22;
|
||||||
|
color: #eee;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn--secondary {
|
||||||
|
background: #333;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn--secondary:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-btn {
|
.admin-btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.45rem 0.9rem;
|
padding: 0.45rem 0.9rem;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe Billing::AdminCompedSubscription do
|
||||||
|
let(:admin) { AdminAccount.create!(username: "ops", password: "secret123") }
|
||||||
|
let(:club) { Club.create!(name: "Sponsor FC", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||||
|
|
||||||
|
before do
|
||||||
|
load Rails.root.join("db/seeds/plans.rb")
|
||||||
|
Billing::AssignPlan.call(club: club, plan_slug: "free")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "concede premium full senza stripe" do
|
||||||
|
described_class.grant(
|
||||||
|
club: club,
|
||||||
|
plan_slug: "premium_full",
|
||||||
|
reason: "Sponsor stagione 2026",
|
||||||
|
admin: admin
|
||||||
|
)
|
||||||
|
|
||||||
|
sub = club.reload.subscription
|
||||||
|
expect(sub.plan.slug).to eq("premium_full")
|
||||||
|
expect(sub.admin_comped?).to be true
|
||||||
|
expect(sub.admin_comped_reason).to eq("Sponsor stagione 2026")
|
||||||
|
expect(sub.admin_comped_by).to eq(admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "revoca omaggio e torna a free" do
|
||||||
|
described_class.grant(club: club, plan_slug: "premium_light", reason: "Promo", admin: admin)
|
||||||
|
|
||||||
|
described_class.revoke(club: club, admin: admin)
|
||||||
|
|
||||||
|
sub = club.reload.subscription
|
||||||
|
expect(sub.plan.slug).to eq("free")
|
||||||
|
expect(sub.admin_comped?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,9 +6,13 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <gtk/gtk_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||||
|
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
gtk
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import app_links
|
||||||
import battery_plus
|
import battery_plus
|
||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
import mobile_scanner
|
import mobile_scanner
|
||||||
@@ -15,6 +16,7 @@ import url_launcher_macos
|
|||||||
import wakelock_plus
|
import wakelock_plus
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
|
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
|
||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <battery_plus/battery_plus_windows_plugin.h>
|
#include <battery_plus/battery_plus_windows_plugin.h>
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
@@ -13,6 +14,8 @@
|
|||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
BatteryPlusWindowsPluginRegisterWithRegistrar(
|
BatteryPlusWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin"));
|
||||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
app_links
|
||||||
battery_plus
|
battery_plus
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
|
|||||||
Reference in New Issue
Block a user