diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb b/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb new file mode 100644 index 000000000..6fa71998a --- /dev/null +++ b/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb @@ -0,0 +1,126 @@ +module Enterprise::Api::V1::Accounts::Concerns::BillingV2 + extend ActiveSupport::Concern + + included do + before_action :validate_topup_amount, only: [:v2_topup] + end + + def credits_balance + service = Enterprise::Billing::V2::CreditManagementService.new(account: @account) + balance = service.credit_balance + + render json: { + id: @account.id, + monthly_credits: balance[:monthly], + topup_credits: balance[:topup], + total_credits: balance[:total], + usage_this_month: balance[:usage_this_month], + usage_total: balance[:usage_total] + } + end + + def credit_grants + service = Enterprise::Billing::V2::CreditManagementService.new(account: @account) + grants = service.fetch_credit_grants + + render json: { credit_grants: grants } + end + + def v2_pricing_plans + plans = Enterprise::Billing::V2::PlanCatalog.plans + render json: { pricing_plans: plans } + end + + def v2_topup_options + options = Enterprise::Billing::V2::TopupCatalog.options + render json: { topup_options: options } + end + + def v2_topup + service = Enterprise::Billing::V2::TopupService.new(account: @account) + result = service.create_topup(credits: params[:credits].to_i) + + if result[:success] + render json: { success: true, message: result[:message] } + else + render json: { error: result[:message] }, status: :unprocessable_entity + end + end + + def v2_subscribe + service = Enterprise::Billing::V2::CheckoutSessionService.new(account: @account) + result = service.create_subscription_checkout( + pricing_plan_id: params[:pricing_plan_id], + quantity: subscription_quantity + ) + + if result[:success] + render json: { success: true, redirect_url: result[:redirect_url], checkout_session_id: result[:checkout_session_id] } + else + render json: { error: result[:message] }, status: :unprocessable_entity + end + end + + def cancel_subscription + service = Enterprise::Billing::V2::CancelSubscriptionService.new(account: @account) + result = service.cancel_subscription + + if result[:success] + # Include account ID and updated attributes for frontend store update + @account.reload + render json: result.merge( + id: @account.id, + custom_attributes: @account.custom_attributes + ) + else + render json: { error: result[:message] }, status: :unprocessable_entity + end + end + + def update_subscription_quantity + service = Enterprise::Billing::V2::UpdateSubscriptionService.new(account: @account) + result = service.update_quantity(quantity: params[:quantity].to_i) + + if result[:success] + # Include account ID and updated attributes for frontend store update + @account.reload + render json: result.merge( + id: @account.id, + custom_attributes: @account.custom_attributes + ) + else + render json: { error: result[:message] }, status: :unprocessable_entity + end + end + + def change_pricing_plan + service = Enterprise::Billing::V2::ChangePlanService.new(account: @account) + result = service.change_plan( + new_pricing_plan_id: params[:pricing_plan_id], + quantity: params[:quantity].to_i + ) + + if result[:success] + # Include account ID and updated attributes for frontend store update + @account.reload + render json: result.merge( + id: @account.id, + custom_attributes: @account.custom_attributes + ) + else + render json: { error: result[:message] }, status: :unprocessable_entity + end + end + + private + + def subscription_quantity + [params[:quantity].to_i, 1].max + end + + def validate_topup_amount + return if params[:credits].to_i.positive? + + render json: { error: 'Topup amount must be greater than 0' }, status: :unprocessable_entity + end +end diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb b/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb index cc819e484..7fed4dfcf 100644 --- a/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb +++ b/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb @@ -1,10 +1,10 @@ -# rubocop:disable Metrics/ClassLength class Enterprise::Api::V1::AccountsController < Api::BaseController include BillingHelper + include Enterprise::Api::V1::Accounts::Concerns::BillingV2 + before_action :fetch_account before_action :check_authorization before_action :check_cloud_env, only: [:limits, :toggle_deletion] - before_action :validate_topup_amount, only: [:v2_topup] def subscription if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank? @@ -57,114 +57,6 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController end end - # V2 Billing Endpoints - def credits_balance - service = Enterprise::Billing::V2::CreditManagementService.new(account: @account) - balance = service.credit_balance - - render json: { - id: @account.id, - monthly_credits: balance[:monthly], - topup_credits: balance[:topup], - total_credits: balance[:total], - usage_this_month: balance[:usage_this_month], - usage_total: balance[:usage_total] - } - end - - def credit_grants - service = Enterprise::Billing::V2::CreditManagementService.new(account: @account) - grants = service.fetch_credit_grants - - render json: { credit_grants: grants } - end - - def v2_pricing_plans - plans = Enterprise::Billing::V2::PlanCatalog.plans - render json: { pricing_plans: plans } - end - - def v2_topup_options - options = Enterprise::Billing::V2::TopupCatalog.options - render json: { topup_options: options } - end - - def v2_topup - service = Enterprise::Billing::V2::TopupService.new(account: @account) - result = service.create_topup(credits: params[:credits].to_i) - - if result[:success] - render json: { success: true, message: result[:message] } - else - render json: { error: result[:message] }, status: :unprocessable_entity - end - end - - def v2_subscribe - service = Enterprise::Billing::V2::CheckoutSessionService.new(account: @account) - result = service.create_subscription_checkout( - pricing_plan_id: params[:pricing_plan_id], - quantity: subscription_quantity - ) - - if result[:success] - render json: { success: true, redirect_url: result[:redirect_url], checkout_session_id: result[:checkout_session_id] } - else - render json: { error: result[:message] }, status: :unprocessable_entity - end - end - - def cancel_subscription - service = Enterprise::Billing::V2::CancelSubscriptionService.new(account: @account) - result = service.cancel_subscription - - if result[:success] - # Include account ID and updated attributes for frontend store update - @account.reload - render json: result.merge( - id: @account.id, - custom_attributes: @account.custom_attributes - ) - else - render json: { error: result[:message] }, status: :unprocessable_entity - end - end - - def update_subscription_quantity - service = Enterprise::Billing::V2::UpdateSubscriptionService.new(account: @account) - result = service.update_quantity(quantity: params[:quantity].to_i) - - if result[:success] - # Include account ID and updated attributes for frontend store update - @account.reload - render json: result.merge( - id: @account.id, - custom_attributes: @account.custom_attributes - ) - else - render json: { error: result[:message] }, status: :unprocessable_entity - end - end - - def change_pricing_plan - service = Enterprise::Billing::V2::ChangePlanService.new(account: @account) - result = service.change_plan( - new_pricing_plan_id: params[:pricing_plan_id], - quantity: params[:quantity].to_i - ) - - if result[:success] - # Include account ID and updated attributes for frontend store update - @account.reload - render json: result.merge( - id: @account.id, - custom_attributes: @account.custom_attributes - ) - else - render json: { error: result[:message] }, status: :unprocessable_entity - end - end - private def check_cloud_env @@ -223,18 +115,6 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController render json: { redirect_url: redirect_url } end - def subscription_quantity - quantity = params[:quantity].to_i - quantity.positive? ? quantity : 1 - end - - def validate_topup_amount - amount = params[:credits].to_i - return if amount.positive? - - render json: { error: 'Topup amount must be greater than 0' }, status: :unprocessable_entity - end - def pundit_user { user: current_user, @@ -243,4 +123,3 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController } end end -# rubocop:enable Metrics/ClassLength diff --git a/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb b/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb index 8c42fb8ef..741ad3b99 100644 --- a/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb @@ -115,7 +115,6 @@ class Enterprise::Billing::V2::SubscriptionProvisioningService < Enterprise::Bil "pricing_plan_id=#{pricing_plan_id}, quantity=#{quantity}" attributes = { - 'stripe_billing_version' => 2, 'stripe_subscription_id' => subscription_id, 'subscribed_quantity' => quantity, 'subscription_status' => 'active', diff --git a/spec/enterprise/services/enterprise/billing/v2/topup_service_spec.rb b/spec/enterprise/services/enterprise/billing/v2/topup_service_spec.rb index e9594fe85..270324dd0 100644 --- a/spec/enterprise/services/enterprise/billing/v2/topup_service_spec.rb +++ b/spec/enterprise/services/enterprise/billing/v2/topup_service_spec.rb @@ -2,8 +2,14 @@ require 'rails_helper' require 'ostruct' describe Enterprise::Billing::V2::TopupService do - let(:account) { create(:account, custom_attributes: { 'stripe_customer_id' => 'cus_123' }) } + let(:account) { create(:account, custom_attributes: { 'stripe_customer_id' => 'cus_123', 'plan_name' => 'Startups' }) } let(:service) { described_class.new(account: account) } + let(:invoice_settings) { instance_double(Stripe::InvoiceSettings, default_payment_method: 'pm_123') } + let(:stripe_customer) do + instance_double(Stripe::Customer, + invoice_settings: invoice_settings, + default_source: nil) + end let(:invoice) { Stripe::Invoice.construct_from(id: 'in_123', customer: 'cus_123', currency: 'usd') } let(:invoice_item) do Stripe::InvoiceItem.construct_from( @@ -30,6 +36,9 @@ describe Enterprise::Billing::V2::TopupService do currency: 'usd' ) + # Mock Stripe Customer retrieval for payment method check + allow(Stripe::Customer).to receive(:retrieve).and_return(stripe_customer) + # Mock Stripe Invoice creation allow(Stripe::Invoice).to receive(:create).and_return(invoice)